commit 719d5a227476173ec911d31e3932032b93bab335 Author: Alatus Lee Date: Sun Sep 28 20:42:16 2025 +0800 '首次提交' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..461f980 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +###################################################################### +# Build Tools + +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +target/ +!.mvn/wrapper/maven-wrapper.jar + +###################################################################### +# IDE + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### JRebel ### +rebel.xml +### NetBeans ### +nbproject/private/ +build/* +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +###################################################################### +# Others +*.log +*.xml.versionsBackup +*.swp + +!*/build/*.java +!*/build/*.html +!*/build/*.xml + +*/test/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd95df1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 若依 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bf2429 --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +

+ logo +

+

RuoYi v3.6.6

+

基于 Vue/Element UI 和 Spring Boot/Spring Cloud & Alibaba 前后端分离的分布式微服务架构

+

+ + + +

+ +## 平台简介 + +若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。 + +* 采用前后端分离的模式,微服务版本前端(基于 [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue))。 +* 后端采用Spring Boot、Spring Cloud & Alibaba。 +* 注册中心、配置中心选型Nacos,权限认证使用Redis。 +* 流量控制框架选型Sentinel,分布式事务选型Seata。 +* 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Cloud-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Cloud-Vue3),保持同步更新。 +* 如需不分离应用,请移步 [RuoYi](https://gitee.com/y_project/RuoYi),如需分离应用,请移步 [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) +* 阿里云优惠券:[点我进入](http://aly.ruoyi.vip),腾讯云优惠券:[点我进入](http://txy.ruoyi.vip)   + +## 系统模块 + +~~~ +com.ruoyi +├── ruoyi-ui // 前端框架 [80] +├── ruoyi-gateway // 网关模块 [8080] +├── ruoyi-auth // 认证中心 [9200] +├── ruoyi-api // 接口模块 +│ └── ruoyi-api-system // 系统接口 +├── ruoyi-common // 通用模块 +│ └── ruoyi-common-core // 核心模块 +│ └── ruoyi-common-datascope // 权限范围 +│ └── ruoyi-common-datasource // 多数据源 +│ └── ruoyi-common-log // 日志记录 +│ └── ruoyi-common-redis // 缓存服务 +│ └── ruoyi-common-seata // 分布式事务 +│ └── ruoyi-common-security // 安全模块 +│ └── ruoyi-common-sensitive // 数据脱敏 +│ └── ruoyi-common-swagger // 系统接口 +├── ruoyi-modules // 业务模块 +│ └── ruoyi-system // 系统模块 [9201] +│ └── ruoyi-gen // 代码生成 [9202] +│ └── ruoyi-job // 定时任务 [9203] +│ └── ruoyi-file // 文件服务 [9300] +├── ruoyi-visual // 图形化管理模块 +│ └── ruoyi-visual-monitor // 监控中心 [9100] +├──pom.xml // 公共依赖 +~~~ + +## 架构图 + + + +## 内置功能 + +1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 +2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。 +3. 岗位管理:配置系统用户所属担任职务。 +4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。 +5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。 +6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。 +7. 参数管理:对系统动态配置常用参数。 +8. 通知公告:系统通知公告信息发布维护。 +9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 +10. 登录日志:系统登录日志记录查询包含登录异常。 +11. 在线用户:当前系统中活跃用户状态监控。 +12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。 +13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。 +14. 系统接口:根据业务代码自动生成相关的api接口文档。 +15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。 +16. 在线构建器:拖动表单元素生成相应的HTML代码。 +17. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。 + +## 在线体验 + +- admin/admin123 +- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。 + +演示地址:http://ruoyi.vip +文档地址:http://doc.ruoyi.vip + +## 演示图 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +## 若依微服务交流群 + +QQ群: [![加入QQ群](https://img.shields.io/badge/已满-42799195-blue.svg)](https://jq.qq.com/?_wv=1027&k=yqInfq0S) [![加入QQ群](https://img.shields.io/badge/已满-170157040-blue.svg)](https://jq.qq.com/?_wv=1027&k=Oy1mb3p8) [![加入QQ群](https://img.shields.io/badge/已满-130643120-blue.svg)](https://jq.qq.com/?_wv=1027&k=rvxkJtXK) [![加入QQ群](https://img.shields.io/badge/已满-225920371-blue.svg)](https://jq.qq.com/?_wv=1027&k=0Ck3PvTe) [![加入QQ群](https://img.shields.io/badge/已满-201705537-blue.svg)](https://jq.qq.com/?_wv=1027&k=FnHHP4TT) [![加入QQ群](https://img.shields.io/badge/已满-236543183-blue.svg)](https://jq.qq.com/?_wv=1027&k=qdT1Ojpz) [![加入QQ群](https://img.shields.io/badge/已满-213618602-blue.svg)](https://jq.qq.com/?_wv=1027&k=nw3OiyXs) [![加入QQ群](https://img.shields.io/badge/已满-148794840-blue.svg)](https://jq.qq.com/?_wv=1027&k=kiU5WDls) [![加入QQ群](https://img.shields.io/badge/已满-118752664-blue.svg)](https://jq.qq.com/?_wv=1027&k=MtBy6YfT) [![加入QQ群](https://img.shields.io/badge/已满-101038945-blue.svg)](https://jq.qq.com/?_wv=1027&k=FqImHgH2) [![加入QQ群](https://img.shields.io/badge/已满-128355254-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G4jZ4EtdT50PhnMBudTnEwgonxkXOscJ&authKey=FkGHYfoTKlGE6wHdKdjH9bVoOgQjtLP9WM%2Fj7pqGY1msoqw9uxDiBo39E2mLgzYg&noverify=0&group_code=128355254) [![加入QQ群](https://img.shields.io/badge/已满-179219821-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=irnwcXhbLOQEv1g-TwGifjNTA_f4wZiA&authKey=4bpzEwhcUY%2FvsPDHvzYn6xfoS%2FtOArvZ%2BGXzfr7O0%2FEqLfkKA%2BuCDXlzHIFg8t93&noverify=0&group_code=179219821) [![加入QQ群](https://img.shields.io/badge/已满-158753145-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=lx1uEdEDuxeM7rUvF3qmlFdqKqdJ5Z-R&authKey=rgyPW9yhhh4IIURKVFa6NgP3qiqH04WAzrJ0trsgkr3pjzm6sKIOGyA58oOjoj%2FJ&noverify=0&group_code=158753145) [![加入QQ群](https://img.shields.io/badge/112869560-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Kuaw0Xdlw2Nlgn6s8h9elzuquHGxGObD&authKey=cSrQcWQ%2BzQZAFFrwxaR%2BbzcumX4WRduZnd1O6JO1dlclQMiu%2BKwxAy8t2JfNp67V&noverify=0&group_code=112869560) 点击按钮入群。 \ No newline at end of file diff --git a/bin/clean.bat b/bin/clean.bat new file mode 100644 index 0000000..24c0974 --- /dev/null +++ b/bin/clean.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] target· +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean + +pause \ No newline at end of file diff --git a/bin/package.bat b/bin/package.bat new file mode 100644 index 0000000..c693ec0 --- /dev/null +++ b/bin/package.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] Weḅwar/jarļ +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean package -Dmaven.test.skip=true + +pause \ No newline at end of file diff --git a/bin/run-auth.bat b/bin/run-auth.bat new file mode 100644 index 0000000..e6cdcce --- /dev/null +++ b/bin/run-auth.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [Ϣ] ʹJarAuth̡ +echo. + +cd %~dp0 +cd ../storm-auth/target + +set JAVA_OPTS=-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -Dfile.encoding=utf-8 %JAVA_OPTS% -jar storm-auth.jar + +cd bin +pause \ No newline at end of file diff --git a/bin/run-gateway.bat b/bin/run-gateway.bat new file mode 100644 index 0000000..a39bb1b --- /dev/null +++ b/bin/run-gateway.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [Ϣ] ʹJarGateway̡ +echo. + +cd %~dp0 +cd ../storm-gateway/target + +set JAVA_OPTS=-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -Dfile.encoding=utf-8 %JAVA_OPTS% -jar storm-gateway.jar + +cd bin +pause \ No newline at end of file diff --git a/bin/run-modules-file.bat b/bin/run-modules-file.bat new file mode 100644 index 0000000..859ab11 --- /dev/null +++ b/bin/run-modules-file.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [Ϣ] ʹJarModules-File̡ +echo. + +cd %~dp0 +cd ../storm-modules/storm-file/target + +set JAVA_OPTS=-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -Dfile.encoding=utf-8 %JAVA_OPTS% -jar storm-modules-file.jar + +cd bin +pause \ No newline at end of file diff --git a/bin/run-modules-gen.bat b/bin/run-modules-gen.bat new file mode 100644 index 0000000..f5cdbf1 --- /dev/null +++ b/bin/run-modules-gen.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [Ϣ] ʹJarModules-Gen̡ +echo. + +cd %~dp0 +cd ../storm-modules/storm-gen/target + +set JAVA_OPTS=-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -Dfile.encoding=utf-8 %JAVA_OPTS% -jar storm-modules-gen.jar + +cd bin +pause \ No newline at end of file diff --git a/bin/run-modules-job.bat b/bin/run-modules-job.bat new file mode 100644 index 0000000..86663b8 --- /dev/null +++ b/bin/run-modules-job.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [Ϣ] ʹJarModules-Job̡ +echo. + +cd %~dp0 +cd ../storm-modules/storm-job/target + +set JAVA_OPTS=-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -Dfile.encoding=utf-8 %JAVA_OPTS% -jar storm-modules-job.jar + +cd bin +pause \ No newline at end of file diff --git a/bin/run-modules-system.bat b/bin/run-modules-system.bat new file mode 100644 index 0000000..79032f0 --- /dev/null +++ b/bin/run-modules-system.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [Ϣ] ʹJarModules-System̡ +echo. + +cd %~dp0 +cd ../storm-modules/storm-system/target + +set JAVA_OPTS=-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -Dfile.encoding=utf-8 %JAVA_OPTS% -jar storm-modules-system.jar + +cd bin +pause \ No newline at end of file diff --git a/bin/run-monitor.bat b/bin/run-monitor.bat new file mode 100644 index 0000000..3b88767 --- /dev/null +++ b/bin/run-monitor.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [Ϣ] ʹJarMonitor̡ +echo. + +cd %~dp0 +cd ../storm-visual/storm-monitor/target + +set JAVA_OPTS=-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -Dfile.encoding=utf-8 %JAVA_OPTS% -jar storm-visual-monitor.jar + +cd bin +pause \ No newline at end of file diff --git a/docker/DATABASE-SETUP.md b/docker/DATABASE-SETUP.md new file mode 100644 index 0000000..ee116ae --- /dev/null +++ b/docker/DATABASE-SETUP.md @@ -0,0 +1,150 @@ +# Storm数据库部署指南 + +## 📋 数据库概览 + +Storm系统使用三个独立的MySQL数据库: + +1. **`storm`** - 主系统数据库 + - 包含用户、角色、菜单、系统配置等核心数据 + - 文件:`dump-storm-202509032128.sql` + +2. **`storm-config`** - 配置数据库 + - 包含Nacos配置中心的数据 + - 文件:`dump-storm-config-202509041019.sql` + +3. **`storm-device`** - 设备数据库 + - 包含设备管理相关的数据 + - 文件:`dump-storm-device-202509032130.sql` + +## 🚀 部署步骤 + +### 1. 确保SQL文件存在 +检查以下文件是否存在于 `mysql/db/` 目录: +``` +mysql/db/ +├── 00-init-all-databases.sql # 数据库初始化脚本 +├── dump-storm-202509032128.sql # 主系统数据库 +├── dump-storm-config-202509041019.sql # 配置数据库 +└── dump-storm-device-202509032130.sql # 设备数据库 +``` + +### 2. 启动MySQL服务 +```bash +# 启动基础服务(包含MySQL) +cd D:\workspace\storm\docker +./deploy.sh base +``` + +### 3. 验证数据库创建 +```bash +# Windows +test-databases.bat + +# Linux/Mac +chmod +x test-databases.sh +./test-databases.sh +``` + +### 4. 手动验证(可选) +```bash +# 连接MySQL +mysql -h localhost -P 3306 -u root -p123456 + +# 查看所有数据库 +SHOW DATABASES; + +# 检查每个数据库的表 +USE storm; +SHOW TABLES; + +USE `storm-config`; +SHOW TABLES; + +USE `storm-device`; +SHOW TABLES; +``` + +## 🔧 故障排除 + +### 问题1:只创建了一个数据库 +**原因**:SQL文件执行顺序问题或文件权限问题 + +**解决方案**: +1. 确保所有SQL文件都在 `mysql/db/` 目录中 +2. 检查文件权限(Linux/Mac) +3. 重新构建MySQL容器: + ```bash + docker-compose down + docker-compose build storm-mysql + docker-compose up -d storm-mysql + ``` + +### 问题2:数据库名称包含特殊字符 +**原因**:`storm-config` 和 `storm-device` 包含连字符 + +**解决方案**: +- 在SQL中使用反引号包围数据库名:`` `storm-config` `` +- 在应用程序连接时也要使用反引号 + +### 问题3:字符集问题 +**原因**:字符集不统一 + +**解决方案**: +- 所有数据库都使用 `utf8mb4` 字符集 +- 确保MySQL容器配置正确 + +## 📊 数据库连接配置 + +### 应用程序配置 +```yaml +# 主系统数据库 +spring: + datasource: + url: jdbc:mysql://storm-mysql:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: 123456 + +# 配置数据库 +nacos: + datasource: + url: jdbc:mysql://storm-mysql:3306/storm-config?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: 123456 + +# 设备数据库 +device: + datasource: + url: jdbc:mysql://storm-mysql:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: 123456 +``` + +## ✅ 验证清单 + +部署完成后,请确认: +- [ ] 三个数据库都已创建 +- [ ] 每个数据库都包含相应的表 +- [ ] 应用程序能正常连接各个数据库 +- [ ] 数据导入成功 +- [ ] 字符集设置正确 + +## 🔄 重新初始化 + +如果需要重新初始化所有数据库: + +```bash +# 停止服务 +docker-compose down + +# 删除MySQL数据卷 +docker volume rm storm_mysql_data + +# 重新启动 +docker-compose up -d storm-mysql + +# 等待初始化完成 +sleep 30 + +# 验证数据库 +test-databases.bat +``` diff --git a/docker/LINUX-MYSQL-FIX.md b/docker/LINUX-MYSQL-FIX.md new file mode 100644 index 0000000..b09e2c5 --- /dev/null +++ b/docker/LINUX-MYSQL-FIX.md @@ -0,0 +1,194 @@ +# Linux服务器MySQL容器多数据库修复指南 + +## 🔍 问题分析 + +在Linux服务器上,MySQL容器的初始化脚本执行顺序可能不稳定,导致只创建了一个数据库。这是因为: + +1. **文件执行顺序不确定**:MySQL容器会按文件名排序执行SQL文件 +2. **数据库创建冲突**:多个SQL文件可能同时尝试创建数据库 +3. **字符集设置问题**:不同文件的字符集设置可能冲突 + +## 🛠️ 解决方案 + +### 方案一:使用编号的初始化脚本(推荐) + +我已经为你创建了以下文件,确保按正确顺序执行: + +``` +mysql/db/ +├── 00-init-all-databases.sql # 全局初始化 +├── 01-init-storm-database.sql # 创建 storm 数据库 +├── 02-init-storm-config-database.sql # 创建 storm-config 数据库 +├── 03-init-storm-device-database.sql # 创建 storm-device 数据库 +├── dump-storm-202509032128.sql # storm 表结构 +├── dump-storm-config-202509041019.sql # storm-config 表结构 +└── dump-storm-device-202509032130.sql # storm-device 表结构 +``` + +### 方案二:修改MySQL Dockerfile + +如果方案一不行,可以修改MySQL Dockerfile: + +```dockerfile +# 基础镜像 +FROM mysql:5.7 + +# 复制初始化脚本 +COPY ./db/00-init-all-databases.sql /docker-entrypoint-initdb.d/00-init-all-databases.sql +COPY ./db/01-init-storm-database.sql /docker-entrypoint-initdb.d/01-init-storm-database.sql +COPY ./db/02-init-storm-config-database.sql /docker-entrypoint-initdb.d/02-init-storm-config-database.sql +COPY ./db/03-init-storm-device-database.sql /docker-entrypoint-initdb.d/03-init-storm-device-database.sql +COPY ./db/dump-storm-202509032128.sql /docker-entrypoint-initdb.d/04-dump-storm.sql +COPY ./db/dump-storm-config-202509041019.sql /docker-entrypoint-initdb.d/05-dump-storm-config.sql +COPY ./db/dump-storm-device-202509032130.sql /docker-entrypoint-initdb.d/06-dump-storm-device.sql +``` + +## 🚀 部署步骤 + +### 1. 清理现有容器和数据 +```bash +# 停止所有服务 +docker-compose down + +# 删除MySQL数据卷(重要!) +docker volume rm storm_mysql_data + +# 或者删除整个数据目录 +sudo rm -rf ./mysql/data/* +``` + +### 2. 重新构建MySQL容器 +```bash +# 重新构建MySQL镜像 +docker-compose build storm-mysql + +# 启动MySQL服务 +docker-compose up -d storm-mysql +``` + +### 3. 等待初始化完成 +```bash +# 查看MySQL容器日志 +docker-compose logs -f storm-mysql + +# 等待看到 "ready for connections" 消息 +``` + +### 4. 验证数据库创建 +```bash +# 连接MySQL检查数据库 +docker exec -it storm-mysql mysql -u root -p123456 -e "SHOW DATABASES;" + +# 应该看到三个数据库: +# +--------------------+ +# | Database | +# +--------------------+ +# | information_schema | +# | mysql | +# | performance_schema | +# | storm | +# | storm-config | +# | storm-device | +# | sys | +# +--------------------+ +``` + +### 5. 检查每个数据库的表 +```bash +# 检查 storm 数据库 +docker exec -it storm-mysql mysql -u root -p123456 -e "USE storm; SHOW TABLES;" + +# 检查 storm-config 数据库 +docker exec -it storm-mysql mysql -u root -p123456 -e "USE \`storm-config\`; SHOW TABLES;" + +# 检查 storm-device 数据库 +docker exec -it storm-mysql mysql -u root -p123456 -e "USE \`storm-device\`; SHOW TABLES;" +``` + +## 🔧 故障排除 + +### 问题1:仍然只创建一个数据库 +**解决方案**: +```bash +# 检查文件权限 +ls -la mysql/db/ + +# 确保所有SQL文件都有执行权限 +chmod 644 mysql/db/*.sql + +# 重新构建容器 +docker-compose down +docker-compose build --no-cache storm-mysql +docker-compose up -d storm-mysql +``` + +### 问题2:数据库名称包含特殊字符 +**解决方案**: +- 在SQL中使用反引号:`` `storm-config` `` +- 在应用程序连接时也要使用反引号 + +### 问题3:字符集问题 +**解决方案**: +```bash +# 检查数据库字符集 +docker exec -it storm-mysql mysql -u root -p123456 -e " +SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME +FROM information_schema.SCHEMATA +WHERE SCHEMA_NAME IN ('storm', 'storm-config', 'storm-device');" +``` + +### 问题4:表结构不完整 +**解决方案**: +```bash +# 检查每个数据库的表数量 +docker exec -it storm-mysql mysql -u root -p123456 -e " +SELECT + TABLE_SCHEMA as 'Database', + COUNT(*) as 'Table Count' +FROM information_schema.TABLES +WHERE TABLE_SCHEMA IN ('storm', 'storm-config', 'storm-device') +GROUP BY TABLE_SCHEMA;" +``` + +## 📋 验证清单 + +部署完成后,请确认: +- [ ] 三个数据库都已创建:`storm`, `storm-config`, `storm-device` +- [ ] 每个数据库都包含相应的表 +- [ ] 字符集设置正确(utf8mb4) +- [ ] 应用程序能正常连接各个数据库 +- [ ] 数据导入成功 + +## 🔄 完全重新初始化 + +如果问题仍然存在,执行完全重新初始化: + +```bash +# 1. 停止所有服务 +docker-compose down + +# 2. 删除所有相关容器和卷 +docker-compose rm -f +docker volume prune -f + +# 3. 删除MySQL数据目录 +sudo rm -rf ./mysql/data/* + +# 4. 重新构建并启动 +docker-compose build --no-cache +docker-compose up -d + +# 5. 等待初始化完成 +sleep 60 + +# 6. 验证结果 +docker exec -it storm-mysql mysql -u root -p123456 -e "SHOW DATABASES;" +``` + +## 📞 技术支持 + +如果问题仍然存在,请提供以下信息: +1. MySQL容器日志:`docker-compose logs storm-mysql` +2. 数据库列表:`docker exec -it storm-mysql mysql -u root -p123456 -e "SHOW DATABASES;"` +3. 文件列表:`ls -la mysql/db/` +4. 服务器操作系统版本 diff --git a/docker/copy.sh b/docker/copy.sh new file mode 100644 index 0000000..b800575 --- /dev/null +++ b/docker/copy.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# 复制项目的文件到对应docker路径,便于一键生成镜像。 +usage() { + echo "Usage: sh copy.sh" + exit 1 +} + + +# copy sql +echo "begin copy sql " +cp ../sql/ry_20250523.sql ./mysql/db +cp ../sql/ry_config_20250224.sql ./mysql/db + +# copy html +echo "begin copy html " +cp -r ../storm-ui/dist/** ./nginx/html/dist + + +# copy jar +echo "begin copy storm-gateway " +cp ../storm-gateway/target/storm-gateway.jar ./storm/gateway/jar + +echo "begin copy storm-auth " +cp ../storm-auth/target/storm-auth.jar ./storm/auth/jar + +echo "begin copy storm-visual " +cp ../storm-visual/storm-monitor/target/storm-visual-monitor.jar ./storm/visual/monitor/jar + +echo "begin copy storm-modules-system " +cp ../storm-modules/storm-system/target/storm-modules-system.jar ./storm/modules/system/jar + +echo "begin copy storm-modules-file " +cp ../storm-modules/storm-file/target/storm-modules-file.jar ./storm/modules/file/jar + +echo "begin copy storm-modules-job " +cp ../storm-modules/storm-job/target/storm-modules-job.jar ./storm/modules/job/jar + +echo "begin copy storm-modules-gen " +cp ../storm-modules/storm-gen/target/storm-modules-gen.jar ./storm/modules/gen/jar + diff --git a/docker/deploy.sh b/docker/deploy.sh new file mode 100644 index 0000000..b752d28 --- /dev/null +++ b/docker/deploy.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +# 使用说明,用来提示输入参数 +usage() { + echo "Usage: sh 执行脚本.sh [port|base|modules|stop|rm]" + exit 1 +} + +# 开启所需端口 +port(){ + firewall-cmd --add-port=80/tcp --permanent + firewall-cmd --add-port=8080/tcp --permanent + firewall-cmd --add-port=8848/tcp --permanent + firewall-cmd --add-port=9848/tcp --permanent + firewall-cmd --add-port=9849/tcp --permanent + firewall-cmd --add-port=6379/tcp --permanent + firewall-cmd --add-port=3306/tcp --permanent + firewall-cmd --add-port=9000/tcp --permanent + firewall-cmd --add-port=9100/tcp --permanent + firewall-cmd --add-port=9200/tcp --permanent + firewall-cmd --add-port=9201/tcp --permanent + firewall-cmd --add-port=9202/tcp --permanent + firewall-cmd --add-port=9203/tcp --permanent + firewall-cmd --add-port=9300/tcp --permanent + firewall-cmd --add-port=443/tcp --permanent + service firewalld restart +} + +# 启动基础环境(必须) +base(){ + docker-compose up -d storm-mysql storm-redis storm-nacos +} + +# 启动程序模块(必须) +modules(){ + docker-compose up -d storm-nginx storm-gateway storm-auth storm-modules-system storm-modules-device +} + +# 关闭所有环境/模块 +stop(){ + docker-compose stop +} + +# 删除所有环境/模块 +rm(){ + docker-compose rm +} + +# 根据输入参数,选择执行对应方法,不输入则执行使用说明 +case "$1" in +"port") + port +;; +"base") + base +;; +"modules") + modules +;; +"stop") + stop +;; +"rm") + rm +;; +*) + usage +;; +esac diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..3e484f5 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,169 @@ +version: '3.8' +services: + storm-nacos: + container_name: storm-nacos + build: + context: ./nacos + environment: + - MODE=standalone + - NACOS_AUTH_TOKEN=wczCOGn5ryZtpt+MdZxv+XxmrER3sYnhE4Mefto8lmc= + - NACOS_AUTH_TOKEN_IGNORE_INVALID_KEY=true + - NACOS_AUTH_IDENTITY_KEY=serverIdentity + - NACOS_AUTH_IDENTITY_VALUE=security + volumes: + - ./nacos/logs/:/home/nacos/logs + - ./nacos/conf/application.properties:/home/nacos/conf/application.properties + ports: + - "8848:8848" + - "9848:9848" + - "9849:9849" + depends_on: + - storm-mysql + networks: + - storm-network + storm-mysql: + container_name: storm-mysql + image: mysql:5.7 + build: + context: ./mysql + ports: + - "3306:3306" + volumes: + - ./mysql/conf:/etc/mysql/conf.d + - ./mysql/logs:/logs + - ./mysql/data:/var/lib/mysql + command: [ + 'mysqld', + '--innodb-buffer-pool-size=80M', + '--character-set-server=utf8mb4', + '--collation-server=utf8mb4_unicode_ci', + '--default-time-zone=+8:00', + '--lower-case-table-names=1' + ] + environment: + MYSQL_DATABASE: 'storm' + MYSQL_ROOT_PASSWORD: 123456 + networks: + - storm-network + storm-redis: + container_name: storm-redis + image: redis + build: + context: ./redis + ports: + - "6379:6379" + volumes: + - ./redis/conf/redis.conf:/usr/local/etc/redis/redis.conf + - ./redis/data:/data + command: redis-server /usr/local/etc/redis/redis.conf + environment: + - REDIS_PASSWORD=123456 # 添加环境变量 + networks: + - storm-network + storm-nginx: + container_name: storm-nginx + image: nginx + build: + context: ./nginx + ports: + - "80:80" + volumes: + - ./nginx/html/dist:/home/storm/projects/storm-ui + - ./nginx/conf/nginx.conf:/etc/nginx/nginx.conf + - ./nginx/logs:/var/log/nginx + - ./nginx/conf.d:/etc/nginx/conf.d + depends_on: + - storm-gateway + networks: + - storm-network + storm-gateway: + container_name: storm-gateway + build: + context: ./storm/gateway + dockerfile: dockerfile + ports: + - "8080:8080" + depends_on: + - storm-redis + networks: + - storm-network + storm-auth: + container_name: storm-auth + build: + context: ./storm/auth + dockerfile: dockerfile + ports: + - "9200:9200" + depends_on: + - storm-redis + networks: + - storm-network + storm-modules-system: + container_name: storm-modules-system + build: + context: ./storm/modules/system + dockerfile: dockerfile + ports: + - "9201:9201" + depends_on: + - storm-redis + - storm-mysql + networks: + - storm-network + storm-modules-gen: + container_name: storm-modules-gen + build: + context: ./storm/modules/gen + dockerfile: dockerfile + ports: + - "9202:9202" + depends_on: + - storm-mysql + networks: + - storm-network + storm-modules-job: + container_name: storm-modules-job + build: + context: ./storm/modules/job + dockerfile: dockerfile + ports: + - "9203:9203" + depends_on: + - storm-mysql + networks: + - storm-network + storm-modules-file: + container_name: storm-modules-file + build: + context: ./storm/modules/file + dockerfile: dockerfile + ports: + - "9300:9300" + volumes: + - ./storm/uploadPath:/home/storm/uploadPath + networks: + - storm-network + storm-visual-monitor: + container_name: storm-visual-monitor + build: + context: ./storm/visual/monitor + dockerfile: dockerfile + ports: + - "9100:9100" + networks: + - storm-network + storm-modules-device: + container_name: storm-modules-device + build: + context: ./storm/modules/device + dockerfile: dockerfile + ports: + - "9000:9000" + depends_on: + - storm-mysql + - storm-redis + networks: + - storm-network +networks: + storm-network: + driver: bridge diff --git a/docker/mysql/db/00-init-all-databases.sql b/docker/mysql/db/00-init-all-databases.sql new file mode 100644 index 0000000..be64ba0 --- /dev/null +++ b/docker/mysql/db/00-init-all-databases.sql @@ -0,0 +1,20 @@ +-- 初始化所有数据库的脚本 +-- 这个脚本会在其他SQL文件之前执行,确保所有数据库都能正确创建 + +-- 设置字符集 +SET NAMES utf8mb4; + +-- 创建 storm 数据库 +DROP DATABASE IF EXISTS `storm`; +CREATE DATABASE `storm` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +-- 创建 storm-config 数据库 +DROP DATABASE IF EXISTS `storm-config`; +CREATE DATABASE `storm-config` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +-- 创建 storm-device 数据库 +DROP DATABASE IF EXISTS `storm-device`; +CREATE DATABASE `storm-device` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +-- 显示创建的数据库 +SHOW DATABASES; diff --git a/docker/mysql/db/01-init-storm-database.sql b/docker/mysql/db/01-init-storm-database.sql new file mode 100644 index 0000000..b9bf892 --- /dev/null +++ b/docker/mysql/db/01-init-storm-database.sql @@ -0,0 +1,15 @@ +-- 初始化 storm 数据库 +-- 这个脚本确保 storm 数据库被正确创建 + +-- 设置字符集 +SET NAMES utf8mb4; + +-- 创建 storm 数据库 +DROP DATABASE IF EXISTS `storm`; +CREATE DATABASE `storm` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +-- 使用 storm 数据库 +USE `storm`; + +-- 设置外键检查 +SET FOREIGN_KEY_CHECKS = 0; diff --git a/docker/mysql/db/02-init-storm-config-database.sql b/docker/mysql/db/02-init-storm-config-database.sql new file mode 100644 index 0000000..3b8b93b --- /dev/null +++ b/docker/mysql/db/02-init-storm-config-database.sql @@ -0,0 +1,15 @@ +-- 初始化 storm-config 数据库 +-- 这个脚本确保 storm-config 数据库被正确创建 + +-- 设置字符集 +SET NAMES utf8mb4; + +-- 创建 storm-config 数据库 +DROP DATABASE IF EXISTS `storm-config`; +CREATE DATABASE `storm-config` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +-- 使用 storm-config 数据库 +USE `storm-config`; + +-- 设置外键检查 +SET FOREIGN_KEY_CHECKS = 0; diff --git a/docker/mysql/db/03-init-storm-device-database.sql b/docker/mysql/db/03-init-storm-device-database.sql new file mode 100644 index 0000000..10cd452 --- /dev/null +++ b/docker/mysql/db/03-init-storm-device-database.sql @@ -0,0 +1,15 @@ +-- 初始化 storm-device 数据库 +-- 这个脚本确保 storm-device 数据库被正确创建 + +-- 设置字符集 +SET NAMES utf8mb4; + +-- 创建 storm-device 数据库 +DROP DATABASE IF EXISTS `storm-device`; +CREATE DATABASE `storm-device` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +-- 使用 storm-device 数据库 +USE `storm-device`; + +-- 设置外键检查 +SET FOREIGN_KEY_CHECKS = 0; diff --git a/docker/mysql/db/dump-storm-202509032128.sql b/docker/mysql/db/dump-storm-202509032128.sql new file mode 100644 index 0000000..54920e1 --- /dev/null +++ b/docker/mysql/db/dump-storm-202509032128.sql @@ -0,0 +1,787 @@ +-- MySQL dump 10.13 Distrib 8.0.42, for Win64 (x86_64) +-- +-- Host: 192.168.48.128 Database: storm +-- ------------------------------------------------------ +-- Server version 5.7.44 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `device` +-- + +-- 数据库已在 01-init-storm-database.sql 中创建 +USE `storm`; + +DROP TABLE IF EXISTS `device`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `device` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `device_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备唯一标识', + `device_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备名称', + `remark` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', + `product_id` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品ID', + `product_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品名称', + `device_type` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备类型', + `location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '安装位置', + `shop_id` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '店铺ID', + `longitude` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '经度', + `latitude` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '纬度', + `software_version` varchar(15) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '软件版本', + `vtxdb_version` varchar(15) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '参数服务器版本', + `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备描述', + `is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建人ID', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新人ID', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_device_id` (`device_id`), + UNIQUE KEY `uk_device_name` (`device_name`), + KEY `idx_product_id` (`product_id`), + KEY `idx_device_type` (`device_type`), + KEY `idx_shop_id` (`shop_id`), + KEY `idx_location` (`location`(50)), + KEY `idx_is_deleted` (`is_deleted`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备基本信息表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `device` +-- + +LOCK TABLES `device` WRITE; +/*!40000 ALTER TABLE `device` DISABLE KEYS */; +/*!40000 ALTER TABLE `device` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `device_status_log` +-- + +DROP TABLE IF EXISTS `device_status_log`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `device_status_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `device_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备ID', + `status` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备状态(ONLINE/OFFLINE)', + `event_time` datetime NOT NULL COMMENT '状态变更时间', + `last_online_time` datetime DEFAULT NULL COMMENT '最后在线时间(仅OFFLINE时有值)', + `duration` bigint(20) DEFAULT NULL COMMENT '本次在线持续时间(秒)', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + PRIMARY KEY (`id`), + KEY `idx_device_id` (`device_id`), + KEY `idx_status` (`status`), + KEY `idx_event_time` (`event_time`), + KEY `idx_device_status` (`device_id`,`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备状态日志表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `device_status_log` +-- + +LOCK TABLES `device_status_log` WRITE; +/*!40000 ALTER TABLE `device_status_log` DISABLE KEYS */; +/*!40000 ALTER TABLE `device_status_log` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `gen_table` +-- + +DROP TABLE IF EXISTS `gen_table`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `gen_table` ( + `table_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号', + `table_name` varchar(200) DEFAULT '' COMMENT '表名称', + `table_comment` varchar(500) DEFAULT '' COMMENT '表描述', + `sub_table_name` varchar(64) DEFAULT NULL COMMENT '关联子表的表名', + `sub_table_fk_name` varchar(64) DEFAULT NULL COMMENT '子表关联的外键名', + `class_name` varchar(100) DEFAULT '' COMMENT '实体类名称', + `tpl_category` varchar(200) DEFAULT 'crud' COMMENT '使用的模板(crud单表操作 tree树表操作)', + `tpl_web_type` varchar(30) DEFAULT '' COMMENT '前端模板类型(element-ui模版 element-plus模版)', + `package_name` varchar(100) DEFAULT NULL COMMENT '生成包路径', + `module_name` varchar(30) DEFAULT NULL COMMENT '生成模块名', + `business_name` varchar(30) DEFAULT NULL COMMENT '生成业务名', + `function_name` varchar(50) DEFAULT NULL COMMENT '生成功能名', + `function_author` varchar(50) DEFAULT NULL COMMENT '生成功能作者', + `gen_type` char(1) DEFAULT '0' COMMENT '生成代码方式(0zip压缩包 1自定义路径)', + `gen_path` varchar(200) DEFAULT '/' COMMENT '生成路径(不填默认项目路径)', + `options` varchar(1000) DEFAULT NULL COMMENT '其它生成选项', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`table_id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='代码生成业务表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `gen_table` +-- + +LOCK TABLES `gen_table` WRITE; +/*!40000 ALTER TABLE `gen_table` DISABLE KEYS */; +INSERT INTO `gen_table` VALUES (1,'device','设备基本信息表',NULL,NULL,'Device','crud','element-plus','com.storm.device','device','device','设备基本信息','storm','0','/','{\"parentMenuId\":1075}','admin','2025-08-22 14:26:37','','2025-08-29 10:05:17',NULL),(2,'device_status_log','设备状态日志表',NULL,NULL,'DeviceStatusLog','crud','element-plus','com.storm.devicestatuslog','devicestatuslog','log','设备状态日志','storm','0','/','{\"parentMenuId\":1068}','admin','2025-08-28 19:57:09','','2025-08-28 20:33:39',NULL),(3,'massage_task','按摩任务记录表',NULL,NULL,'MassageTask','crud','element-plus','com.storm.massage','massage','massage','按摩任务记录','storm','0','/','{\"parentMenuId\":1068}','admin','2025-08-28 19:57:09','','2025-08-28 20:32:13',NULL); +/*!40000 ALTER TABLE `gen_table` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `gen_table_column` +-- + +DROP TABLE IF EXISTS `gen_table_column`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `gen_table_column` ( + `column_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号', + `table_id` bigint(20) DEFAULT NULL COMMENT '归属表编号', + `column_name` varchar(200) DEFAULT NULL COMMENT '列名称', + `column_comment` varchar(500) DEFAULT NULL COMMENT '列描述', + `column_type` varchar(100) DEFAULT NULL COMMENT '列类型', + `java_type` varchar(500) DEFAULT NULL COMMENT 'JAVA类型', + `java_field` varchar(200) DEFAULT NULL COMMENT 'JAVA字段名', + `is_pk` char(1) DEFAULT NULL COMMENT '是否主键(1是)', + `is_increment` char(1) DEFAULT NULL COMMENT '是否自增(1是)', + `is_required` char(1) DEFAULT NULL COMMENT '是否必填(1是)', + `is_insert` char(1) DEFAULT NULL COMMENT '是否为插入字段(1是)', + `is_edit` char(1) DEFAULT NULL COMMENT '是否编辑字段(1是)', + `is_list` char(1) DEFAULT NULL COMMENT '是否列表字段(1是)', + `is_query` char(1) DEFAULT NULL COMMENT '是否查询字段(1是)', + `query_type` varchar(200) DEFAULT 'EQ' COMMENT '查询方式(等于、不等于、大于、小于、范围)', + `html_type` varchar(200) DEFAULT NULL COMMENT '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)', + `dict_type` varchar(200) DEFAULT '' COMMENT '字典类型', + `sort` int(11) DEFAULT NULL COMMENT '排序', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`column_id`) +) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8mb4 COMMENT='代码生成业务表字段'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `gen_table_column` +-- + +LOCK TABLES `gen_table_column` WRITE; +/*!40000 ALTER TABLE `gen_table_column` DISABLE KEYS */; +INSERT INTO `gen_table_column` VALUES (1,1,'id',NULL,'bigint(20)','Long','id','1','1',NULL,'0',NULL,NULL,NULL,'EQ','input','',1,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(2,1,'device_id','设备唯一标识','varchar(100)','String','deviceId','0','0','0','1','0','0','1','EQ','input','',2,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(3,1,'device_name','设备名称','varchar(100)','String','deviceName','0','0','0','1','1','1','1','LIKE','input','',3,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(4,1,'note_name','备注名称','varchar(100)','String','noteName','0','0',NULL,'1','1','1','1','LIKE','input','',4,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(5,1,'product_id','产品ID','varchar(100)','String','productId','0','0',NULL,'1','1','1','1','EQ','input','',5,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(6,1,'product_name','产品名称','varchar(100)','String','productName','0','0',NULL,'1','1','1','1','LIKE','input','',6,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(7,1,'device_type','设备类型','varchar(50)','String','deviceType','0','0',NULL,'1','1','1','1','EQ','select','',7,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(8,1,'location','安装位置','varchar(255)','String','location','0','0',NULL,'1','1','1','1','EQ','input','',8,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(9,1,'shop_id','店铺ID','varchar(100)','String','shopId','0','0',NULL,'1','1','1','1','EQ','input','',9,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(10,1,'longitude','经度','varchar(20)','String','longitude','0','0',NULL,'1','1','0','0','EQ','input','',10,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(11,1,'latitude','纬度','varchar(20)','String','latitude','0','0',NULL,'1','1','0','0','EQ','input','',11,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(12,1,'software_version','软件版本','varchar(15)','String','softwareVersion','0','0',NULL,'1','1','0','0','EQ','input','',12,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(13,1,'vtxdb_version','参数服务器版本','varchar(15)','String','vtxdbVersion','0','0',NULL,'1','1','0','0','EQ','input','',13,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(14,1,'description','设备描述','varchar(500)','String','description','0','0',NULL,'1','1','0','0','EQ','textarea','',14,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(15,1,'is_deleted','是否删除','tinyint(1)','Integer','isDeleted','0','0',NULL,'0','0','0','0','EQ','input','',15,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(16,1,'create_by','创建人ID','bigint(20)','Long','createBy','0','0',NULL,'0',NULL,NULL,NULL,'EQ','input','',16,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(17,1,'update_by','更新人ID','bigint(20)','Long','updateBy','0','0',NULL,'0','0',NULL,NULL,'EQ','input','',17,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(18,1,'create_time','创建时间','datetime','LocalDateTime','createTime','0','0',NULL,'0',NULL,'1','1','EQ','datetime','',18,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(19,1,'update_time','更新时间','datetime','LocalDateTime','updateTime','0','0',NULL,'0','0','1','1','EQ','datetime','',19,'admin','2025-08-22 14:26:37','','2025-08-29 10:05:17'),(20,2,'id',NULL,'bigint(20)','Long','id','1','1',NULL,'0',NULL,NULL,NULL,'EQ','input','',1,'admin','2025-08-28 19:57:09','','2025-08-28 20:33:39'),(21,2,'device_id','设备ID','varchar(100)','String','deviceId','0','0','0','1','1','1','1','EQ','input','',2,'admin','2025-08-28 19:57:09','','2025-08-28 20:33:39'),(22,2,'status','设备状态(ONLINE/OFFLINE)','varchar(10)','String','status','0','0','0','1','1','1','1','EQ','radio','',3,'admin','2025-08-28 19:57:09','','2025-08-28 20:33:39'),(23,2,'event_time','状态变更时间','datetime','LocalDateTime','eventTime','0','0','0','1','1','1','1','EQ','datetime','',4,'admin','2025-08-28 19:57:09','','2025-08-28 20:33:39'),(24,2,'last_online_time','最后在线时间(仅OFFLINE时有值)','datetime','LocalDateTime','lastOnlineTime','0','0',NULL,'1','1','1','1','EQ','datetime','',5,'admin','2025-08-28 19:57:09','','2025-08-28 20:33:39'),(25,2,'duration','本次在线持续时间(秒)','bigint(20)','Long','duration','0','0',NULL,'1','1','1','1','EQ','input','',6,'admin','2025-08-28 19:57:09','','2025-08-28 20:33:39'),(26,2,'create_time','记录创建时间','datetime','LocalDateTime','createTime','0','0',NULL,'0',NULL,NULL,NULL,'EQ','datetime','',7,'admin','2025-08-28 19:57:09','','2025-08-28 20:33:39'),(27,3,'id','主键ID','bigint(20)','Long','id','1','1',NULL,'0',NULL,NULL,NULL,'EQ','input','',1,'admin','2025-08-28 19:57:09','','2025-08-28 20:32:13'),(28,3,'device_id','设备ID','varchar(100)','String','deviceId','0','0','0','1','1','1','1','EQ','input','',2,'admin','2025-08-28 19:57:09','','2025-08-28 20:32:13'),(29,3,'device_name','设备名称(冗余存储)','varchar(100)','String','deviceName','0','0',NULL,'1','1','1','1','LIKE','input','',3,'admin','2025-08-28 19:57:09','','2025-08-28 20:32:13'),(30,3,'product_name','产品型号(冗余存储)','varchar(100)','String','productName','0','0',NULL,'1','1','1','1','LIKE','input','',4,'admin','2025-08-28 19:57:09','','2025-08-28 20:32:13'),(31,3,'head_type','按摩头类型','varchar(50)','String','headType','0','0','0','1','1','1','1','EQ','select','',5,'admin','2025-08-28 19:57:09','','2025-08-28 20:32:13'),(32,3,'body_part','按摩部位','varchar(50)','String','bodyPart','0','0','0','1','1','1','0','EQ','input','',6,'admin','2025-08-28 19:57:09','','2025-08-28 20:32:13'),(33,3,'task_time','本次按摩持续时间(秒)','int(11)','Long','taskTime','0','0','0','1','1','1','1','EQ','input','',7,'admin','2025-08-28 19:57:09','','2025-08-28 20:32:13'),(34,3,'start_time','开始时间','datetime','LocalDateTime','startTime','0','0','0','0','0','0','0','EQ','datetime','',8,'admin','2025-08-28 19:57:09','','2025-08-28 20:32:13'),(35,3,'end_time','结束时间','datetime','LocalDateTime','endTime','0','0','0','0','0','0','0','EQ','datetime','',9,'admin','2025-08-28 19:57:09','','2025-08-28 20:32:13'),(36,3,'create_time','记录创建时间','datetime','LocalDateTime','createTime','0','0',NULL,'0',NULL,NULL,NULL,'EQ','datetime','',10,'admin','2025-08-28 19:57:09','','2025-08-28 20:32:13'); +/*!40000 ALTER TABLE `gen_table_column` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `massage_task` +-- + +DROP TABLE IF EXISTS `massage_task`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `massage_task` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `device_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备ID', + `device_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备名称(冗余存储)', + `product_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品型号(冗余存储)', + `head_type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '按摩头类型', + `body_part` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '按摩部位', + `task_time` int(11) NOT NULL COMMENT '本次按摩持续时间(秒)', + `start_time` datetime NOT NULL COMMENT '开始时间', + `end_time` datetime NOT NULL COMMENT '结束时间', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + PRIMARY KEY (`id`), + KEY `idx_device_id` (`device_id`), + KEY `idx_start_time` (`start_time`), + KEY `idx_end_time` (`end_time`), + KEY `idx_device_time` (`device_id`,`start_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='按摩任务记录表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `massage_task` +-- + +LOCK TABLES `massage_task` WRITE; +/*!40000 ALTER TABLE `massage_task` DISABLE KEYS */; +/*!40000 ALTER TABLE `massage_task` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_config` +-- + +DROP TABLE IF EXISTS `sys_config`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_config` ( + `config_id` int(5) NOT NULL AUTO_INCREMENT COMMENT '参数主键', + `config_name` varchar(100) DEFAULT '' COMMENT '参数名称', + `config_key` varchar(100) DEFAULT '' COMMENT '参数键名', + `config_value` varchar(500) DEFAULT '' COMMENT '参数键值', + `config_type` char(1) DEFAULT 'N' COMMENT '系统内置(Y是 N否)', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`config_id`) +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COMMENT='参数配置表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_config` +-- + +LOCK TABLES `sys_config` WRITE; +/*!40000 ALTER TABLE `sys_config` DISABLE KEYS */; +INSERT INTO `sys_config` VALUES (1,'主框架页-默认皮肤样式名称','sys.index.skinName','skin-blue','Y','admin','2025-08-20 10:54:10','',NULL,'蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow'),(2,'用户管理-账号初始密码','sys.user.initPassword','123456','Y','admin','2025-08-20 10:54:10','',NULL,'初始化密码 123456'),(3,'主框架页-侧边栏主题','sys.index.sideTheme','theme-dark','Y','admin','2025-08-20 10:54:10','',NULL,'深色主题theme-dark,浅色主题theme-light'),(4,'账号自助-是否开启用户注册功能','sys.account.registerUser','false','Y','admin','2025-08-20 10:54:10','',NULL,'是否开启注册用户功能(true开启,false关闭)'),(5,'用户登录-黑名单列表','sys.login.blackIPList','','Y','admin','2025-08-20 10:54:10','',NULL,'设置登录IP黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)'),(6,'用户管理-初始密码修改策略','sys.account.initPasswordModify','1','Y','admin','2025-08-20 10:54:10','',NULL,'0:初始密码修改策略关闭,没有任何提示,1:提醒用户,如果未修改初始密码,则在登录时就会提醒修改密码对话框'),(7,'用户管理-账号密码更新周期','sys.account.passwordValidateDays','0','Y','admin','2025-08-20 10:54:10','',NULL,'密码更新周期(填写数字,数据初始化值为0不限制,若修改必须为大于0小于365的正整数),如果超过这个周期登录系统时,则在登录时就会提醒修改密码对话框'); +/*!40000 ALTER TABLE `sys_config` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_dept` +-- + +DROP TABLE IF EXISTS `sys_dept`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_dept` ( + `dept_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '部门id', + `parent_id` bigint(20) DEFAULT '0' COMMENT '父部门id', + `ancestors` varchar(50) DEFAULT '' COMMENT '祖级列表', + `dept_name` varchar(30) DEFAULT '' COMMENT '部门名称', + `order_num` int(4) DEFAULT '0' COMMENT '显示顺序', + `leader` varchar(20) DEFAULT NULL COMMENT '负责人', + `phone` varchar(11) DEFAULT NULL COMMENT '联系电话', + `email` varchar(50) DEFAULT NULL COMMENT '邮箱', + `status` char(1) DEFAULT '0' COMMENT '部门状态(0正常 1停用)', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`dept_id`) +) ENGINE=InnoDB AUTO_INCREMENT=110 DEFAULT CHARSET=utf8mb4 COMMENT='部门表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_dept` +-- + +LOCK TABLES `sys_dept` WRITE; +/*!40000 ALTER TABLE `sys_dept` DISABLE KEYS */; +INSERT INTO `sys_dept` VALUES (101,100,'0,100','深圳总公司',1,'具身风暴','15888888888','fb@qq.com','0','0','admin','2025-08-20 10:54:10','',NULL),(102,100,'0,100','广州分公司',2,'具身风暴','15888888888','fb@qq.com','0','0','admin','2025-08-20 10:54:10','',NULL),(103,101,'0,100,101','研发部门',1,'具身风暴','15888888888','fb@qq.com','0','0','admin','2025-08-20 10:54:10','',NULL),(104,101,'0,100,101','市场部门',2,'具身风暴','15888888888','fb@qq.com','0','0','admin','2025-08-20 10:54:10','',NULL),(105,101,'0,100,101','测试部门',3,'具身风暴','15888888888','fb@qq.com','0','0','admin','2025-08-20 10:54:10','',NULL),(106,101,'0,100,101','财务部门',4,'具身风暴','15888888888','fb@qq.com','0','0','admin','2025-08-20 10:54:10','',NULL),(107,101,'0,100,101','运维部门',5,'具身风暴','15888888888','fb@qq.com','0','0','admin','2025-08-20 10:54:10','',NULL),(108,102,'0,100,102','市场部门',1,'具身风暴','15888888888','fb@qq.com','0','0','admin','2025-08-20 10:54:10','',NULL),(109,102,'0,100,102','财务部门',2,'具身风暴','15888888888','fb@qq.com','0','0','admin','2025-08-20 10:54:10','',NULL); +/*!40000 ALTER TABLE `sys_dept` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_dict_data` +-- + +DROP TABLE IF EXISTS `sys_dict_data`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_dict_data` ( + `dict_code` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '字典编码', + `dict_sort` int(4) DEFAULT '0' COMMENT '字典排序', + `dict_label` varchar(100) DEFAULT '' COMMENT '字典标签', + `dict_value` varchar(100) DEFAULT '' COMMENT '字典键值', + `dict_type` varchar(100) DEFAULT '' COMMENT '字典类型', + `css_class` varchar(100) DEFAULT NULL COMMENT '样式属性(其他样式扩展)', + `list_class` varchar(100) DEFAULT NULL COMMENT '表格回显样式', + `is_default` char(1) DEFAULT 'N' COMMENT '是否默认(Y是 N否)', + `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`dict_code`) +) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COMMENT='字典数据表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_dict_data` +-- + +LOCK TABLES `sys_dict_data` WRITE; +/*!40000 ALTER TABLE `sys_dict_data` DISABLE KEYS */; +INSERT INTO `sys_dict_data` VALUES (1,1,'男','0','sys_user_sex','','','Y','0','admin','2025-08-20 10:54:10','',NULL,'性别男'),(2,2,'女','1','sys_user_sex','','','N','0','admin','2025-08-20 10:54:10','',NULL,'性别女'),(3,3,'未知','2','sys_user_sex','','','N','0','admin','2025-08-20 10:54:10','',NULL,'性别未知'),(4,1,'显示','0','sys_show_hide','','primary','Y','0','admin','2025-08-20 10:54:10','',NULL,'显示菜单'),(5,2,'隐藏','1','sys_show_hide','','danger','N','0','admin','2025-08-20 10:54:10','',NULL,'隐藏菜单'),(6,1,'正常','0','sys_normal_disable','','primary','Y','0','admin','2025-08-20 10:54:10','',NULL,'正常状态'),(7,2,'停用','1','sys_normal_disable','','danger','N','0','admin','2025-08-20 10:54:10','',NULL,'停用状态'),(8,1,'正常','0','sys_job_status','','primary','Y','0','admin','2025-08-20 10:54:10','',NULL,'正常状态'),(9,2,'暂停','1','sys_job_status','','danger','N','0','admin','2025-08-20 10:54:10','',NULL,'停用状态'),(10,1,'默认','DEFAULT','sys_job_group','','','Y','0','admin','2025-08-20 10:54:10','',NULL,'默认分组'),(11,2,'系统','SYSTEM','sys_job_group','','','N','0','admin','2025-08-20 10:54:10','',NULL,'系统分组'),(12,1,'是','Y','sys_yes_no','','primary','Y','0','admin','2025-08-20 10:54:10','',NULL,'系统默认是'),(13,2,'否','N','sys_yes_no','','danger','N','0','admin','2025-08-20 10:54:10','',NULL,'系统默认否'),(14,1,'通知','1','sys_notice_type','','warning','Y','0','admin','2025-08-20 10:54:10','',NULL,'通知'),(15,2,'公告','2','sys_notice_type','','success','N','0','admin','2025-08-20 10:54:10','',NULL,'公告'),(16,1,'正常','0','sys_notice_status','','primary','Y','0','admin','2025-08-20 10:54:10','',NULL,'正常状态'),(17,2,'关闭','1','sys_notice_status','','danger','N','0','admin','2025-08-20 10:54:10','',NULL,'关闭状态'),(18,99,'其他','0','sys_oper_type','','info','N','0','admin','2025-08-20 10:54:10','',NULL,'其他操作'),(19,1,'新增','1','sys_oper_type','','info','N','0','admin','2025-08-20 10:54:10','',NULL,'新增操作'),(20,2,'修改','2','sys_oper_type','','info','N','0','admin','2025-08-20 10:54:10','',NULL,'修改操作'),(21,3,'删除','3','sys_oper_type','','danger','N','0','admin','2025-08-20 10:54:10','',NULL,'删除操作'),(22,4,'授权','4','sys_oper_type','','primary','N','0','admin','2025-08-20 10:54:10','',NULL,'授权操作'),(23,5,'导出','5','sys_oper_type','','warning','N','0','admin','2025-08-20 10:54:10','',NULL,'导出操作'),(24,6,'导入','6','sys_oper_type','','warning','N','0','admin','2025-08-20 10:54:10','',NULL,'导入操作'),(25,7,'强退','7','sys_oper_type','','danger','N','0','admin','2025-08-20 10:54:10','',NULL,'强退操作'),(26,8,'生成代码','8','sys_oper_type','','warning','N','0','admin','2025-08-20 10:54:10','',NULL,'生成操作'),(27,9,'清空数据','9','sys_oper_type','','danger','N','0','admin','2025-08-20 10:54:10','',NULL,'清空操作'),(28,1,'成功','0','sys_common_status','','primary','N','0','admin','2025-08-20 10:54:10','',NULL,'正常状态'),(29,2,'失败','1','sys_common_status','','danger','N','0','admin','2025-08-20 10:54:10','',NULL,'停用状态'); +/*!40000 ALTER TABLE `sys_dict_data` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_dict_type` +-- + +DROP TABLE IF EXISTS `sys_dict_type`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_dict_type` ( + `dict_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '字典主键', + `dict_name` varchar(100) DEFAULT '' COMMENT '字典名称', + `dict_type` varchar(100) DEFAULT '' COMMENT '字典类型', + `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`dict_id`), + UNIQUE KEY `dict_type` (`dict_type`) +) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COMMENT='字典类型表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_dict_type` +-- + +LOCK TABLES `sys_dict_type` WRITE; +/*!40000 ALTER TABLE `sys_dict_type` DISABLE KEYS */; +INSERT INTO `sys_dict_type` VALUES (1,'用户性别','sys_user_sex','0','admin','2025-08-20 10:54:10','',NULL,'用户性别列表'),(2,'菜单状态','sys_show_hide','0','admin','2025-08-20 10:54:10','',NULL,'菜单状态列表'),(3,'系统开关','sys_normal_disable','0','admin','2025-08-20 10:54:10','',NULL,'系统开关列表'),(4,'任务状态','sys_job_status','0','admin','2025-08-20 10:54:10','',NULL,'任务状态列表'),(5,'任务分组','sys_job_group','0','admin','2025-08-20 10:54:10','',NULL,'任务分组列表'),(6,'系统是否','sys_yes_no','0','admin','2025-08-20 10:54:10','',NULL,'系统是否列表'),(7,'通知类型','sys_notice_type','0','admin','2025-08-20 10:54:10','',NULL,'通知类型列表'),(8,'通知状态','sys_notice_status','0','admin','2025-08-20 10:54:10','',NULL,'通知状态列表'),(9,'操作类型','sys_oper_type','0','admin','2025-08-20 10:54:10','',NULL,'操作类型列表'),(10,'系统状态','sys_common_status','0','admin','2025-08-20 10:54:10','',NULL,'登录状态列表'); +/*!40000 ALTER TABLE `sys_dict_type` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_job` +-- + +DROP TABLE IF EXISTS `sys_job`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_job` ( + `job_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务ID', + `job_name` varchar(64) NOT NULL DEFAULT '' COMMENT '任务名称', + `job_group` varchar(64) NOT NULL DEFAULT 'DEFAULT' COMMENT '任务组名', + `invoke_target` varchar(500) NOT NULL COMMENT '调用目标字符串', + `cron_expression` varchar(255) DEFAULT '' COMMENT 'cron执行表达式', + `misfire_policy` varchar(20) DEFAULT '3' COMMENT '计划执行错误策略(1立即执行 2执行一次 3放弃执行)', + `concurrent` char(1) DEFAULT '1' COMMENT '是否并发执行(0允许 1禁止)', + `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1暂停)', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT '' COMMENT '备注信息', + PRIMARY KEY (`job_id`,`job_name`,`job_group`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='定时任务调度表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_job` +-- + +LOCK TABLES `sys_job` WRITE; +/*!40000 ALTER TABLE `sys_job` DISABLE KEYS */; +INSERT INTO `sys_job` VALUES (1,'系统默认(无参)','DEFAULT','ryTask.ryNoParams','0/10 * * * * ?','3','1','1','admin','2025-08-20 10:54:10','',NULL,''),(2,'系统默认(有参)','DEFAULT','ryTask.ryParams(\'ry\')','0/15 * * * * ?','3','1','1','admin','2025-08-20 10:54:10','',NULL,''),(3,'系统默认(多参)','DEFAULT','ryTask.ryMultipleParams(\'ry\', true, 2000L, 316.50D, 100)','0/20 * * * * ?','3','1','1','admin','2025-08-20 10:54:10','',NULL,''); +/*!40000 ALTER TABLE `sys_job` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_job_log` +-- + +DROP TABLE IF EXISTS `sys_job_log`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_job_log` ( + `job_log_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务日志ID', + `job_name` varchar(64) NOT NULL COMMENT '任务名称', + `job_group` varchar(64) NOT NULL COMMENT '任务组名', + `invoke_target` varchar(500) NOT NULL COMMENT '调用目标字符串', + `job_message` varchar(500) DEFAULT NULL COMMENT '日志信息', + `status` char(1) DEFAULT '0' COMMENT '执行状态(0正常 1失败)', + `exception_info` varchar(2000) DEFAULT '' COMMENT '异常信息', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`job_log_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定时任务调度日志表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_job_log` +-- + +LOCK TABLES `sys_job_log` WRITE; +/*!40000 ALTER TABLE `sys_job_log` DISABLE KEYS */; +/*!40000 ALTER TABLE `sys_job_log` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_logininfor` +-- + +DROP TABLE IF EXISTS `sys_logininfor`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_logininfor` ( + `info_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '访问ID', + `user_name` varchar(50) DEFAULT '' COMMENT '用户账号', + `ipaddr` varchar(128) DEFAULT '' COMMENT '登录IP地址', + `status` char(1) DEFAULT '0' COMMENT '登录状态(0成功 1失败)', + `msg` varchar(255) DEFAULT '' COMMENT '提示信息', + `access_time` datetime DEFAULT NULL COMMENT '访问时间', + PRIMARY KEY (`info_id`), + KEY `idx_sys_logininfor_s` (`status`), + KEY `idx_sys_logininfor_lt` (`access_time`) +) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8mb4 COMMENT='系统访问记录'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_logininfor` +-- + +LOCK TABLES `sys_logininfor` WRITE; +/*!40000 ALTER TABLE `sys_logininfor` DISABLE KEYS */; +INSERT INTO `sys_logininfor` VALUES (1,'admin','127.0.0.1','0','登录成功','2025-08-21 11:08:32'),(2,'admin','127.0.0.1','0','退出成功','2025-08-22 14:26:05'),(3,'admin','127.0.0.1','0','登录成功','2025-08-22 14:26:12'),(4,'admin','127.0.0.1','0','退出成功','2025-08-22 19:02:59'),(5,'admin','127.0.0.1','0','登录成功','2025-08-22 19:03:04'),(6,'admin','127.0.0.1','0','退出成功','2025-08-23 15:01:50'),(7,'admin','127.0.0.1','0','登录成功','2025-08-23 15:01:59'),(8,'admin','127.0.0.1','0','登录成功','2025-08-25 11:45:25'),(9,'admin','127.0.0.1','0','登录成功','2025-08-28 19:55:35'),(10,'admin','127.0.0.1','0','登录成功','2025-09-01 10:18:36'),(11,'admin','127.0.0.1','0','退出成功','2025-09-02 14:09:29'),(12,'admin','127.0.0.1','0','登录成功','2025-09-02 14:10:03'),(13,'admin','127.0.0.1','0','退出成功','2025-09-02 14:10:33'),(14,'admin','127.0.0.1','0','登录成功','2025-09-02 14:10:59'),(15,'admin','127.0.0.1','0','登录成功','2025-09-03 11:24:16'),(16,'admin','127.0.0.1','0','退出成功','2025-09-03 14:03:57'),(17,'admin','127.0.0.1','0','登录成功','2025-09-03 14:04:01'),(18,'admin','127.0.0.1','0','退出成功','2025-09-03 18:01:07'),(19,'dz','127.0.0.1','0','登录成功','2025-09-03 18:01:12'),(20,'dz','127.0.0.1','0','退出成功','2025-09-03 18:01:22'),(21,'admin','127.0.0.1','0','登录成功','2025-09-03 18:01:29'),(22,'admin','127.0.0.1','0','退出成功','2025-09-03 18:03:07'),(23,'user','127.0.0.1','1','密码输入错误1次','2025-09-03 18:03:13'),(24,'user','127.0.0.1','0','登录成功','2025-09-03 18:03:19'),(25,'user','127.0.0.1','0','退出成功','2025-09-03 18:03:35'),(26,'admin','127.0.0.1','0','登录成功','2025-09-03 18:03:43'),(27,'admin','127.0.0.1','0','退出成功','2025-09-03 20:16:55'),(28,'user','127.0.0.1','1','密码输入错误1次','2025-09-03 20:17:32'),(29,'user','127.0.0.1','1','密码输入错误2次','2025-09-03 20:17:35'),(30,'user','127.0.0.1','0','登录成功','2025-09-03 20:17:41'),(31,'user','127.0.0.1','0','退出成功','2025-09-03 20:21:07'),(32,'admin','127.0.0.1','0','登录成功','2025-09-03 20:21:22'),(33,'admin','127.0.0.1','0','退出成功','2025-09-03 20:21:49'),(34,'admin','127.0.0.1','0','登录成功','2025-09-03 20:23:00'),(35,'admin','127.0.0.1','0','退出成功','2025-09-03 20:23:07'); +/*!40000 ALTER TABLE `sys_logininfor` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_menu` +-- + +DROP TABLE IF EXISTS `sys_menu`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_menu` ( + `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `menu_name` varchar(50) NOT NULL COMMENT '菜单名称', + `parent_id` bigint(20) DEFAULT '0' COMMENT '父菜单ID', + `order_num` int(4) DEFAULT '0' COMMENT '显示顺序', + `path` varchar(200) DEFAULT '' COMMENT '路由地址', + `component` varchar(255) DEFAULT NULL COMMENT '组件路径', + `query` varchar(255) DEFAULT NULL COMMENT '路由参数', + `route_name` varchar(50) DEFAULT '' COMMENT '路由名称', + `is_frame` int(1) DEFAULT '1' COMMENT '是否为外链(0是 1否)', + `is_cache` int(1) DEFAULT '0' COMMENT '是否缓存(0缓存 1不缓存)', + `menu_type` char(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)', + `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)', + `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)', + `perms` varchar(100) DEFAULT NULL COMMENT '权限标识', + `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT '' COMMENT '备注', + PRIMARY KEY (`menu_id`) +) ENGINE=InnoDB AUTO_INCREMENT=1082 DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_menu` +-- + +LOCK TABLES `sys_menu` WRITE; +/*!40000 ALTER TABLE `sys_menu` DISABLE KEYS */; +INSERT INTO `sys_menu` VALUES (1,'系统管理',0,1,'system',NULL,'','',1,0,'M','0','0','','system','admin','2025-08-20 10:54:10','',NULL,'系统管理目录'),(2,'系统监控',0,2,'monitor',NULL,'','',1,0,'M','0','0','','monitor','admin','2025-08-20 10:54:10','',NULL,'系统监控目录'),(3,'系统工具',0,3,'tool',NULL,'','',1,0,'M','0','0','','tool','admin','2025-08-20 10:54:10','',NULL,'系统工具目录'),(100,'用户管理',1,1,'user','system/user/index','','',1,0,'C','0','0','system:user:list','user','admin','2025-08-20 10:54:10','',NULL,'用户管理菜单'),(101,'角色管理',1,2,'role','system/role/index','','',1,0,'C','0','0','system:role:list','peoples','admin','2025-08-20 10:54:10','',NULL,'角色管理菜单'),(102,'菜单管理',1,3,'menu','system/menu/index','','',1,0,'C','0','0','system:menu:list','tree-table','admin','2025-08-20 10:54:10','',NULL,'菜单管理菜单'),(103,'部门管理',1,4,'dept','system/dept/index','','',1,0,'C','0','0','system:dept:list','tree','admin','2025-08-20 10:54:10','',NULL,'部门管理菜单'),(104,'岗位管理',1,5,'post','system/post/index','','',1,0,'C','0','0','system:post:list','post','admin','2025-08-20 10:54:10','',NULL,'岗位管理菜单'),(105,'字典管理',1,6,'dict','system/dict/index','','',1,0,'C','0','0','system:dict:list','dict','admin','2025-08-20 10:54:10','',NULL,'字典管理菜单'),(106,'参数设置',1,7,'config','system/config/index','','',1,0,'C','0','0','system:config:list','edit','admin','2025-08-20 10:54:10','',NULL,'参数设置菜单'),(107,'通知公告',1,8,'notice','system/notice/index','','',1,0,'C','0','0','system:notice:list','message','admin','2025-08-20 10:54:10','',NULL,'通知公告菜单'),(108,'日志管理',1,9,'log','','','',1,0,'M','0','0','','log','admin','2025-08-20 10:54:10','',NULL,'日志管理菜单'),(109,'在线用户',2,1,'online','monitor/online/index','','',1,0,'C','0','0','monitor:online:list','online','admin','2025-08-20 10:54:10','',NULL,'在线用户菜单'),(110,'定时任务',2,2,'job','monitor/job/index','','',1,0,'C','0','0','monitor:job:list','job','admin','2025-08-20 10:54:10','',NULL,'定时任务菜单'),(111,'Sentinel控制台',2,3,'http://localhost:8718','','','',0,0,'C','0','0','monitor:sentinel:list','sentinel','admin','2025-08-20 10:54:10','',NULL,'流量控制菜单'),(112,'Nacos控制台',2,4,'http://localhost:8848/nacos','','','',0,0,'C','0','0','monitor:nacos:list','nacos','admin','2025-08-20 10:54:10','',NULL,'服务治理菜单'),(113,'Admin控制台',2,5,'http://localhost:9100/login','','','',0,0,'C','0','0','monitor:server:list','server','admin','2025-08-20 10:54:10','',NULL,'服务监控菜单'),(114,'表单构建',3,1,'build','tool/build/index','','',1,0,'C','0','0','tool:build:list','build','admin','2025-08-20 10:54:10','',NULL,'表单构建菜单'),(115,'代码生成',3,2,'gen','tool/gen/index','','',1,0,'C','0','0','tool:gen:list','code','admin','2025-08-20 10:54:10','',NULL,'代码生成菜单'),(116,'系统接口',3,3,'http://localhost:8080/swagger-ui/index.html','','','',0,0,'C','0','0','tool:swagger:list','swagger','admin','2025-08-20 10:54:10','',NULL,'系统接口菜单'),(500,'操作日志',108,1,'operlog','system/operlog/index','','',1,0,'C','0','0','system:operlog:list','form','admin','2025-08-20 10:54:10','',NULL,'操作日志菜单'),(501,'登录日志',108,2,'logininfor','system/logininfor/index','','',1,0,'C','0','0','system:logininfor:list','logininfor','admin','2025-08-20 10:54:10','',NULL,'登录日志菜单'),(1000,'用户查询',100,1,'','','','',1,0,'F','0','0','system:user:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1001,'用户新增',100,2,'','','','',1,0,'F','0','0','system:user:add','#','admin','2025-08-20 10:54:10','',NULL,''),(1002,'用户修改',100,3,'','','','',1,0,'F','0','0','system:user:edit','#','admin','2025-08-20 10:54:10','',NULL,''),(1003,'用户删除',100,4,'','','','',1,0,'F','0','0','system:user:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1004,'用户导出',100,5,'','','','',1,0,'F','0','0','system:user:export','#','admin','2025-08-20 10:54:10','',NULL,''),(1005,'用户导入',100,6,'','','','',1,0,'F','0','0','system:user:import','#','admin','2025-08-20 10:54:10','',NULL,''),(1006,'重置密码',100,7,'','','','',1,0,'F','0','0','system:user:resetPwd','#','admin','2025-08-20 10:54:10','',NULL,''),(1007,'角色查询',101,1,'','','','',1,0,'F','0','0','system:role:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1008,'角色新增',101,2,'','','','',1,0,'F','0','0','system:role:add','#','admin','2025-08-20 10:54:10','',NULL,''),(1009,'角色修改',101,3,'','','','',1,0,'F','0','0','system:role:edit','#','admin','2025-08-20 10:54:10','',NULL,''),(1010,'角色删除',101,4,'','','','',1,0,'F','0','0','system:role:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1011,'角色导出',101,5,'','','','',1,0,'F','0','0','system:role:export','#','admin','2025-08-20 10:54:10','',NULL,''),(1012,'菜单查询',102,1,'','','','',1,0,'F','0','0','system:menu:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1013,'菜单新增',102,2,'','','','',1,0,'F','0','0','system:menu:add','#','admin','2025-08-20 10:54:10','',NULL,''),(1014,'菜单修改',102,3,'','','','',1,0,'F','0','0','system:menu:edit','#','admin','2025-08-20 10:54:10','',NULL,''),(1015,'菜单删除',102,4,'','','','',1,0,'F','0','0','system:menu:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1016,'部门查询',103,1,'','','','',1,0,'F','0','0','system:dept:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1017,'部门新增',103,2,'','','','',1,0,'F','0','0','system:dept:add','#','admin','2025-08-20 10:54:10','',NULL,''),(1018,'部门修改',103,3,'','','','',1,0,'F','0','0','system:dept:edit','#','admin','2025-08-20 10:54:10','',NULL,''),(1019,'部门删除',103,4,'','','','',1,0,'F','0','0','system:dept:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1020,'岗位查询',104,1,'','','','',1,0,'F','0','0','system:post:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1021,'岗位新增',104,2,'','','','',1,0,'F','0','0','system:post:add','#','admin','2025-08-20 10:54:10','',NULL,''),(1022,'岗位修改',104,3,'','','','',1,0,'F','0','0','system:post:edit','#','admin','2025-08-20 10:54:10','',NULL,''),(1023,'岗位删除',104,4,'','','','',1,0,'F','0','0','system:post:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1024,'岗位导出',104,5,'','','','',1,0,'F','0','0','system:post:export','#','admin','2025-08-20 10:54:10','',NULL,''),(1025,'字典查询',105,1,'#','','','',1,0,'F','0','0','system:dict:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1026,'字典新增',105,2,'#','','','',1,0,'F','0','0','system:dict:add','#','admin','2025-08-20 10:54:10','',NULL,''),(1027,'字典修改',105,3,'#','','','',1,0,'F','0','0','system:dict:edit','#','admin','2025-08-20 10:54:10','',NULL,''),(1028,'字典删除',105,4,'#','','','',1,0,'F','0','0','system:dict:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1029,'字典导出',105,5,'#','','','',1,0,'F','0','0','system:dict:export','#','admin','2025-08-20 10:54:10','',NULL,''),(1030,'参数查询',106,1,'#','','','',1,0,'F','0','0','system:config:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1031,'参数新增',106,2,'#','','','',1,0,'F','0','0','system:config:add','#','admin','2025-08-20 10:54:10','',NULL,''),(1032,'参数修改',106,3,'#','','','',1,0,'F','0','0','system:config:edit','#','admin','2025-08-20 10:54:10','',NULL,''),(1033,'参数删除',106,4,'#','','','',1,0,'F','0','0','system:config:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1034,'参数导出',106,5,'#','','','',1,0,'F','0','0','system:config:export','#','admin','2025-08-20 10:54:10','',NULL,''),(1035,'公告查询',107,1,'#','','','',1,0,'F','0','0','system:notice:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1036,'公告新增',107,2,'#','','','',1,0,'F','0','0','system:notice:add','#','admin','2025-08-20 10:54:10','',NULL,''),(1037,'公告修改',107,3,'#','','','',1,0,'F','0','0','system:notice:edit','#','admin','2025-08-20 10:54:10','',NULL,''),(1038,'公告删除',107,4,'#','','','',1,0,'F','0','0','system:notice:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1039,'操作查询',500,1,'#','','','',1,0,'F','0','0','system:operlog:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1040,'操作删除',500,2,'#','','','',1,0,'F','0','0','system:operlog:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1041,'日志导出',500,3,'#','','','',1,0,'F','0','0','system:operlog:export','#','admin','2025-08-20 10:54:10','',NULL,''),(1042,'登录查询',501,1,'#','','','',1,0,'F','0','0','system:logininfor:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1043,'登录删除',501,2,'#','','','',1,0,'F','0','0','system:logininfor:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1044,'日志导出',501,3,'#','','','',1,0,'F','0','0','system:logininfor:export','#','admin','2025-08-20 10:54:10','',NULL,''),(1045,'账户解锁',501,4,'#','','','',1,0,'F','0','0','system:logininfor:unlock','#','admin','2025-08-20 10:54:10','',NULL,''),(1046,'在线查询',109,1,'#','','','',1,0,'F','0','0','monitor:online:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1047,'批量强退',109,2,'#','','','',1,0,'F','0','0','monitor:online:batchLogout','#','admin','2025-08-20 10:54:10','',NULL,''),(1048,'单条强退',109,3,'#','','','',1,0,'F','0','0','monitor:online:forceLogout','#','admin','2025-08-20 10:54:10','',NULL,''),(1049,'任务查询',110,1,'#','','','',1,0,'F','0','0','monitor:job:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1050,'任务新增',110,2,'#','','','',1,0,'F','0','0','monitor:job:add','#','admin','2025-08-20 10:54:10','',NULL,''),(1051,'任务修改',110,3,'#','','','',1,0,'F','0','0','monitor:job:edit','#','admin','2025-08-20 10:54:10','',NULL,''),(1052,'任务删除',110,4,'#','','','',1,0,'F','0','0','monitor:job:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1053,'状态修改',110,5,'#','','','',1,0,'F','0','0','monitor:job:changeStatus','#','admin','2025-08-20 10:54:10','',NULL,''),(1054,'任务导出',110,6,'#','','','',1,0,'F','0','0','monitor:job:export','#','admin','2025-08-20 10:54:10','',NULL,''),(1055,'生成查询',115,1,'#','','','',1,0,'F','0','0','tool:gen:query','#','admin','2025-08-20 10:54:10','',NULL,''),(1056,'生成修改',115,2,'#','','','',1,0,'F','0','0','tool:gen:edit','#','admin','2025-08-20 10:54:10','',NULL,''),(1057,'生成删除',115,3,'#','','','',1,0,'F','0','0','tool:gen:remove','#','admin','2025-08-20 10:54:10','',NULL,''),(1058,'导入代码',115,2,'#','','','',1,0,'F','0','0','tool:gen:import','#','admin','2025-08-20 10:54:10','',NULL,''),(1059,'预览代码',115,4,'#','','','',1,0,'F','0','0','tool:gen:preview','#','admin','2025-08-20 10:54:10','',NULL,''),(1060,'生成代码',115,5,'#','','','',1,0,'F','0','0','tool:gen:code','#','admin','2025-08-20 10:54:10','',NULL,''),(1075,'设备管理',0,1,'devicemanage',NULL,NULL,'',1,0,'M','0','0',NULL,'client','admin','2025-08-29 10:04:21','',NULL,''),(1076,'设备基本信息',1075,1,'device','device/device/index',NULL,'',1,0,'C','0','0','device:device:list','#','admin','2025-08-29 10:06:40','',NULL,'设备基本信息菜单'),(1077,'设备基本信息查询',1076,1,'#','',NULL,'',1,0,'F','0','0','device:device:query','#','admin','2025-08-29 10:06:40','',NULL,''),(1078,'设备基本信息新增',1076,2,'#','',NULL,'',1,0,'F','0','0','device:device:add','#','admin','2025-08-29 10:06:40','',NULL,''),(1079,'设备基本信息修改',1076,3,'#','',NULL,'',1,0,'F','0','0','device:device:edit','#','admin','2025-08-29 10:06:40','',NULL,''),(1080,'设备基本信息删除',1076,4,'#','',NULL,'',1,0,'F','0','0','device:device:remove','#','admin','2025-08-29 10:06:40','',NULL,''),(1081,'设备基本信息导出',1076,5,'#','',NULL,'',1,0,'F','0','0','device:device:export','#','admin','2025-08-29 10:06:40','',NULL,''); +/*!40000 ALTER TABLE `sys_menu` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_notice` +-- + +DROP TABLE IF EXISTS `sys_notice`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_notice` ( + `notice_id` int(4) NOT NULL AUTO_INCREMENT COMMENT '公告ID', + `notice_title` varchar(50) NOT NULL COMMENT '公告标题', + `notice_type` char(1) NOT NULL COMMENT '公告类型(1通知 2公告)', + `notice_content` longblob COMMENT '公告内容', + `status` char(1) DEFAULT '0' COMMENT '公告状态(0正常 1关闭)', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(255) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`notice_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知公告表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_notice` +-- + +LOCK TABLES `sys_notice` WRITE; +/*!40000 ALTER TABLE `sys_notice` DISABLE KEYS */; +/*!40000 ALTER TABLE `sys_notice` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_oper_log` +-- + +DROP TABLE IF EXISTS `sys_oper_log`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_oper_log` ( + `oper_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键', + `title` varchar(50) DEFAULT '' COMMENT '模块标题', + `business_type` int(2) DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)', + `method` varchar(200) DEFAULT '' COMMENT '方法名称', + `request_method` varchar(10) DEFAULT '' COMMENT '请求方式', + `operator_type` int(1) DEFAULT '0' COMMENT '操作类别(0其它 1后台用户 2手机端用户)', + `oper_name` varchar(50) DEFAULT '' COMMENT '操作人员', + `dept_name` varchar(50) DEFAULT '' COMMENT '部门名称', + `oper_url` varchar(255) DEFAULT '' COMMENT '请求URL', + `oper_ip` varchar(128) DEFAULT '' COMMENT '主机地址', + `oper_location` varchar(255) DEFAULT '' COMMENT '操作地点', + `oper_param` varchar(2000) DEFAULT '' COMMENT '请求参数', + `json_result` varchar(2000) DEFAULT '' COMMENT '返回参数', + `status` int(1) DEFAULT '0' COMMENT '操作状态(0正常 1异常)', + `error_msg` varchar(2000) DEFAULT '' COMMENT '错误消息', + `oper_time` datetime DEFAULT NULL COMMENT '操作时间', + `cost_time` bigint(20) DEFAULT '0' COMMENT '消耗时间', + PRIMARY KEY (`oper_id`), + KEY `idx_sys_oper_log_bt` (`business_type`), + KEY `idx_sys_oper_log_s` (`status`), + KEY `idx_sys_oper_log_ot` (`oper_time`) +) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COMMENT='操作日志记录'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_oper_log` +-- + +LOCK TABLES `sys_oper_log` WRITE; +/*!40000 ALTER TABLE `sys_oper_log` DISABLE KEYS */; +INSERT INTO `sys_oper_log` VALUES (1,'代码生成',6,'com.storm.gen.controller.GenController.importTableSave()','POST',1,'admin',NULL,'/gen/importTable','127.0.0.1','','{\"tables\":\"device\"}','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-22 14:26:37',82),(2,'代码生成',2,'com.storm.gen.controller.GenController.editSave()','PUT',1,'admin',NULL,'/gen','127.0.0.1','','{\"businessName\":\"device\",\"className\":\"Device\",\"columns\":[{\"capJavaField\":\"Id\",\"columnId\":1,\"columnName\":\"id\",\"columnType\":\"bigint(20)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":true,\"insert\":false,\"isIncrement\":\"1\",\"isInsert\":\"0\",\"isPk\":\"1\",\"javaField\":\"id\",\"javaType\":\"Long\",\"list\":false,\"params\":{},\"pk\":true,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":1,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"usableColumn\":false},{\"capJavaField\":\"DeviceId\",\"columnComment\":\"设备唯一标识\",\"columnId\":2,\"columnName\":\"device_id\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"0\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceId\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"EQ\",\"required\":false,\"sort\":2,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"usableColumn\":false},{\"capJavaField\":\"DeviceName\",\"columnComment\":\"设备名称\",\"columnId\":3,\"columnName\":\"device_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceName\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"LIKE\",\"required\":false,\"sort\":3,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"usableColumn\":false},{\"capJavaField\":\"NoteName\",\"columnComment\":\"备注名称\",\"columnId\":4,\"columnName\":\"note_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"javaField\":\"noteName\",\"javaType\":\"String\",\"list\":tr','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-22 14:45:09',145),(3,'代码生成',2,'com.storm.gen.controller.GenController.editSave()','PUT',1,'admin',NULL,'/gen','127.0.0.1','','{\"businessName\":\"device\",\"className\":\"Device\",\"columns\":[{\"capJavaField\":\"Id\",\"columnId\":1,\"columnName\":\"id\",\"columnType\":\"bigint(20)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":true,\"insert\":false,\"isIncrement\":\"1\",\"isInsert\":\"0\",\"isPk\":\"1\",\"javaField\":\"id\",\"javaType\":\"Long\",\"list\":false,\"params\":{},\"pk\":true,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":1,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-22 14:45:09\",\"usableColumn\":false},{\"capJavaField\":\"DeviceId\",\"columnComment\":\"设备唯一标识\",\"columnId\":2,\"columnName\":\"device_id\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"0\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceId\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"EQ\",\"required\":false,\"sort\":2,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-22 14:45:09\",\"usableColumn\":false},{\"capJavaField\":\"DeviceName\",\"columnComment\":\"设备名称\",\"columnId\":3,\"columnName\":\"device_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceName\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"LIKE\",\"required\":false,\"sort\":3,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-22 14:45:09\",\"usableColumn\":false},{\"capJavaField\":\"NoteName\",\"columnComment\":\"备注名称\",\"columnId\":4,\"columnName\":\"note_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-22 14:46:52',52),(4,'代码生成',8,'com.storm.gen.controller.GenController.batchGenCode()','GET',1,'admin',NULL,'/gen/batchGenCode','127.0.0.1','','{\"tables\":\"device\"}',NULL,0,NULL,'2025-08-22 14:46:55',522),(5,'代码生成',6,'com.storm.gen.controller.GenController.importTableSave()','POST',1,'admin',NULL,'/gen/importTable','127.0.0.1','','{\"tables\":\"massage_task,device_status_log\"}','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-28 19:57:09',98),(6,'代码生成',2,'com.storm.gen.controller.GenController.editSave()','PUT',1,'admin',NULL,'/gen','127.0.0.1','','{\"businessName\":\"task\",\"className\":\"MassageTask\",\"columns\":[{\"capJavaField\":\"Id\",\"columnComment\":\"主键ID\",\"columnId\":27,\"columnName\":\"id\",\"columnType\":\"bigint(20)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":true,\"insert\":false,\"isIncrement\":\"1\",\"isInsert\":\"0\",\"isPk\":\"1\",\"javaField\":\"id\",\"javaType\":\"Long\",\"list\":false,\"params\":{},\"pk\":true,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":1,\"superColumn\":false,\"tableId\":3,\"updateBy\":\"\",\"usableColumn\":false},{\"capJavaField\":\"DeviceId\",\"columnComment\":\"设备ID\",\"columnId\":28,\"columnName\":\"device_id\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"0\",\"isRequired\":\"0\",\"javaField\":\"deviceId\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":2,\"superColumn\":false,\"tableId\":3,\"updateBy\":\"\",\"usableColumn\":false},{\"capJavaField\":\"DeviceName\",\"columnComment\":\"设备名称(冗余存储)\",\"columnId\":29,\"columnName\":\"device_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"javaField\":\"deviceName\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"LIKE\",\"required\":false,\"sort\":3,\"superColumn\":false,\"tableId\":3,\"updateBy\":\"\",\"usableColumn\":false},{\"capJavaField\":\"ProductName\",\"columnComment\":\"产品型号(冗余存储)\",\"columnId\":30,\"columnName\":\"product_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"javaField\":\"productNam','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-28 19:58:53',93),(7,'菜单管理',1,'com.storm.system.controller.SysMenuController.add()','POST',1,'admin',NULL,'/menu','127.0.0.1','','{\"children\":[],\"createBy\":\"admin\",\"icon\":\"client\",\"isCache\":\"0\",\"isFrame\":\"1\",\"menuName\":\"设备管理\",\"menuType\":\"M\",\"orderNum\":1,\"params\":{},\"parentId\":0,\"path\":\"device\",\"status\":\"0\",\"visible\":\"0\"}','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-28 20:02:24',28),(8,'菜单管理',2,'com.storm.system.controller.SysMenuController.edit()','PUT',1,'admin',NULL,'/menu','127.0.0.1','','{\"children\":[],\"component\":\"device/device/index\",\"createTime\":\"2025-08-22 16:58:58\",\"icon\":\"#\",\"isCache\":\"0\",\"isFrame\":\"1\",\"menuId\":1061,\"menuName\":\"设备基本信息\",\"menuType\":\"C\",\"orderNum\":1,\"params\":{},\"parentId\":1067,\"path\":\"device\",\"perms\":\"device:device:list\",\"routeName\":\"\",\"status\":\"0\",\"updateBy\":\"admin\",\"visible\":\"0\"}','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-28 20:02:38',19),(9,'代码生成',2,'com.storm.gen.controller.GenController.editSave()','PUT',1,'admin',NULL,'/gen','127.0.0.1','','{\"businessName\":\"device\",\"className\":\"Device\",\"columns\":[{\"capJavaField\":\"Id\",\"columnId\":1,\"columnName\":\"id\",\"columnType\":\"bigint(20)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":true,\"insert\":false,\"isIncrement\":\"1\",\"isInsert\":\"0\",\"isPk\":\"1\",\"javaField\":\"id\",\"javaType\":\"Long\",\"list\":false,\"params\":{},\"pk\":true,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":1,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-22 14:46:52\",\"usableColumn\":false},{\"capJavaField\":\"DeviceId\",\"columnComment\":\"设备唯一标识\",\"columnId\":2,\"columnName\":\"device_id\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"0\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"0\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceId\",\"javaType\":\"String\",\"list\":false,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"EQ\",\"required\":false,\"sort\":2,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-22 14:46:52\",\"usableColumn\":false},{\"capJavaField\":\"DeviceName\",\"columnComment\":\"设备名称\",\"columnId\":3,\"columnName\":\"device_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceName\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"LIKE\",\"required\":false,\"sort\":3,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-22 14:46:52\",\"usableColumn\":false},{\"capJavaField\":\"NoteName\",\"columnComment\":\"备注名称\",\"columnId\":4,\"columnName\":\"note_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\"','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-28 20:28:54',51),(10,'菜单管理',1,'com.storm.system.controller.SysMenuController.add()','POST',1,'admin',NULL,'/menu','127.0.0.1','','{\"children\":[],\"createBy\":\"admin\",\"icon\":\"client\",\"isCache\":\"0\",\"isFrame\":\"1\",\"menuName\":\"设备管理\",\"menuType\":\"M\",\"orderNum\":1,\"params\":{},\"parentId\":0,\"path\":\"device\",\"status\":\"0\",\"visible\":\"0\"}','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-28 20:29:57',5),(11,'代码生成',2,'com.storm.gen.controller.GenController.editSave()','PUT',1,'admin',NULL,'/gen','127.0.0.1','','{\"businessName\":\"device\",\"className\":\"Device\",\"columns\":[{\"capJavaField\":\"Id\",\"columnId\":1,\"columnName\":\"id\",\"columnType\":\"bigint(20)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":true,\"insert\":false,\"isIncrement\":\"1\",\"isInsert\":\"0\",\"isPk\":\"1\",\"javaField\":\"id\",\"javaType\":\"Long\",\"list\":false,\"params\":{},\"pk\":true,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":1,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 20:28:54\",\"usableColumn\":false},{\"capJavaField\":\"DeviceId\",\"columnComment\":\"设备唯一标识\",\"columnId\":2,\"columnName\":\"device_id\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"0\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"0\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceId\",\"javaType\":\"String\",\"list\":false,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"EQ\",\"required\":false,\"sort\":2,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 20:28:54\",\"usableColumn\":false},{\"capJavaField\":\"DeviceName\",\"columnComment\":\"设备名称\",\"columnId\":3,\"columnName\":\"device_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceName\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"LIKE\",\"required\":false,\"sort\":3,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 20:28:54\",\"usableColumn\":false},{\"capJavaField\":\"NoteName\",\"columnComment\":\"备注名称\",\"columnId\":4,\"columnName\":\"note_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\"','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-28 20:30:21',44),(12,'代码生成',8,'com.storm.gen.controller.GenController.batchGenCode()','GET',1,'admin',NULL,'/gen/batchGenCode','127.0.0.1','','{\"tables\":\"device\"}',NULL,0,NULL,'2025-08-28 20:30:26',657),(13,'代码生成',2,'com.storm.gen.controller.GenController.editSave()','PUT',1,'admin',NULL,'/gen','127.0.0.1','','{\"businessName\":\"massage\",\"className\":\"MassageTask\",\"columns\":[{\"capJavaField\":\"Id\",\"columnComment\":\"主键ID\",\"columnId\":27,\"columnName\":\"id\",\"columnType\":\"bigint(20)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":true,\"insert\":false,\"isIncrement\":\"1\",\"isInsert\":\"0\",\"isPk\":\"1\",\"javaField\":\"id\",\"javaType\":\"Long\",\"list\":false,\"params\":{},\"pk\":true,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":1,\"superColumn\":false,\"tableId\":3,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 19:58:53\",\"usableColumn\":false},{\"capJavaField\":\"DeviceId\",\"columnComment\":\"设备ID\",\"columnId\":28,\"columnName\":\"device_id\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"0\",\"isRequired\":\"0\",\"javaField\":\"deviceId\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":2,\"superColumn\":false,\"tableId\":3,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 19:58:53\",\"usableColumn\":false},{\"capJavaField\":\"DeviceName\",\"columnComment\":\"设备名称(冗余存储)\",\"columnId\":29,\"columnName\":\"device_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"javaField\":\"deviceName\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"LIKE\",\"required\":false,\"sort\":3,\"superColumn\":false,\"tableId\":3,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 19:58:53\",\"usableColumn\":false},{\"capJavaField\":\"ProductName\",\"columnComment\":\"产品型号(冗余存储)\",\"columnId\":30,\"columnName\":\"product_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-28 20:31:40',23),(14,'代码生成',2,'com.storm.gen.controller.GenController.editSave()','PUT',1,'admin',NULL,'/gen','127.0.0.1','','{\"businessName\":\"massage\",\"className\":\"MassageTask\",\"columns\":[{\"capJavaField\":\"Id\",\"columnComment\":\"主键ID\",\"columnId\":27,\"columnName\":\"id\",\"columnType\":\"bigint(20)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":true,\"insert\":false,\"isIncrement\":\"1\",\"isInsert\":\"0\",\"isPk\":\"1\",\"javaField\":\"id\",\"javaType\":\"Long\",\"list\":false,\"params\":{},\"pk\":true,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":1,\"superColumn\":false,\"tableId\":3,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 20:31:40\",\"usableColumn\":false},{\"capJavaField\":\"DeviceId\",\"columnComment\":\"设备ID\",\"columnId\":28,\"columnName\":\"device_id\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceId\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"EQ\",\"required\":false,\"sort\":2,\"superColumn\":false,\"tableId\":3,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 20:31:40\",\"usableColumn\":false},{\"capJavaField\":\"DeviceName\",\"columnComment\":\"设备名称(冗余存储)\",\"columnId\":29,\"columnName\":\"device_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"javaField\":\"deviceName\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"LIKE\",\"required\":false,\"sort\":3,\"superColumn\":false,\"tableId\":3,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 20:31:40\",\"usableColumn\":false},{\"capJavaField\":\"ProductName\",\"columnComment\":\"产品型号(冗余存储)\",\"columnId\":30,\"columnName\":\"product_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-28 20:32:13',25),(15,'代码生成',8,'com.storm.gen.controller.GenController.batchGenCode()','GET',1,'admin',NULL,'/gen/batchGenCode','127.0.0.1','','{\"tables\":\"massage_task\"}',NULL,0,NULL,'2025-08-28 20:32:16',276),(16,'代码生成',2,'com.storm.gen.controller.GenController.editSave()','PUT',1,'admin',NULL,'/gen','127.0.0.1','','{\"businessName\":\"log\",\"className\":\"DeviceStatusLog\",\"columns\":[{\"capJavaField\":\"Id\",\"columnId\":20,\"columnName\":\"id\",\"columnType\":\"bigint(20)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":true,\"insert\":false,\"isIncrement\":\"1\",\"isInsert\":\"0\",\"isPk\":\"1\",\"javaField\":\"id\",\"javaType\":\"Long\",\"list\":false,\"params\":{},\"pk\":true,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":1,\"superColumn\":false,\"tableId\":2,\"updateBy\":\"\",\"usableColumn\":false},{\"capJavaField\":\"DeviceId\",\"columnComment\":\"设备ID\",\"columnId\":21,\"columnName\":\"device_id\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceId\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"EQ\",\"required\":false,\"sort\":2,\"superColumn\":false,\"tableId\":2,\"updateBy\":\"\",\"usableColumn\":false},{\"capJavaField\":\"Status\",\"columnComment\":\"设备状态(ONLINE/OFFLINE)\",\"columnId\":22,\"columnName\":\"status\",\"columnType\":\"varchar(10)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"radio\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"status\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"EQ\",\"required\":false,\"sort\":3,\"superColumn\":false,\"tableId\":2,\"updateBy\":\"\",\"usableColumn\":false},{\"capJavaField\":\"EventTime\",\"columnComment\":\"状态变更时间\",\"columnId\":23,\"columnName\":\"event_time\",\"columnType\":\"datetime\",\"createBy\":\"admin\",\"createTime\":\"2025-08-28 19:57:09\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"datetime\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"eventTime\",\"','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-28 20:33:39',25),(17,'代码生成',8,'com.storm.gen.controller.GenController.batchGenCode()','GET',1,'admin',NULL,'/gen/batchGenCode','127.0.0.1','','{\"tables\":\"device_status_log\"}',NULL,0,NULL,'2025-08-28 20:33:42',112),(18,'菜单管理',1,'com.storm.system.controller.SysMenuController.add()','POST',1,'admin',NULL,'/menu','127.0.0.1','','{\"children\":[],\"createBy\":\"admin\",\"icon\":\"client\",\"isCache\":\"0\",\"isFrame\":\"1\",\"menuName\":\"设备管理\",\"menuType\":\"M\",\"orderNum\":1,\"params\":{},\"parentId\":0,\"path\":\"devicemanage\",\"status\":\"0\",\"visible\":\"0\"}','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-29 10:04:21',10),(19,'代码生成',2,'com.storm.gen.controller.GenController.editSave()','PUT',1,'admin',NULL,'/gen','127.0.0.1','','{\"businessName\":\"device\",\"className\":\"Device\",\"columns\":[{\"capJavaField\":\"Id\",\"columnId\":1,\"columnName\":\"id\",\"columnType\":\"bigint(20)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":true,\"insert\":false,\"isIncrement\":\"1\",\"isInsert\":\"0\",\"isPk\":\"1\",\"javaField\":\"id\",\"javaType\":\"Long\",\"list\":false,\"params\":{},\"pk\":true,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":1,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 20:30:21\",\"usableColumn\":false},{\"capJavaField\":\"DeviceId\",\"columnComment\":\"设备唯一标识\",\"columnId\":2,\"columnName\":\"device_id\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"0\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"0\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceId\",\"javaType\":\"String\",\"list\":false,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"EQ\",\"required\":false,\"sort\":2,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 20:30:21\",\"usableColumn\":false},{\"capJavaField\":\"DeviceName\",\"columnComment\":\"设备名称\",\"columnId\":3,\"columnName\":\"device_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceName\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"LIKE\",\"required\":false,\"sort\":3,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-28 20:30:21\",\"usableColumn\":false},{\"capJavaField\":\"NoteName\",\"columnComment\":\"备注名称\",\"columnId\":4,\"columnName\":\"note_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\"','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-29 10:05:06',43),(20,'代码生成',2,'com.storm.gen.controller.GenController.editSave()','PUT',1,'admin',NULL,'/gen','127.0.0.1','','{\"businessName\":\"device\",\"className\":\"Device\",\"columns\":[{\"capJavaField\":\"Id\",\"columnId\":1,\"columnName\":\"id\",\"columnType\":\"bigint(20)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":true,\"insert\":false,\"isIncrement\":\"1\",\"isInsert\":\"0\",\"isPk\":\"1\",\"javaField\":\"id\",\"javaType\":\"Long\",\"list\":false,\"params\":{},\"pk\":true,\"query\":false,\"queryType\":\"EQ\",\"required\":false,\"sort\":1,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-29 10:05:06\",\"usableColumn\":false},{\"capJavaField\":\"DeviceId\",\"columnComment\":\"设备唯一标识\",\"columnId\":2,\"columnName\":\"device_id\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":false,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"0\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"0\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceId\",\"javaType\":\"String\",\"list\":false,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"EQ\",\"required\":false,\"sort\":2,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-29 10:05:06\",\"usableColumn\":false},{\"capJavaField\":\"DeviceName\",\"columnComment\":\"设备名称\",\"columnId\":3,\"columnName\":\"device_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\",\"isInsert\":\"1\",\"isList\":\"1\",\"isPk\":\"0\",\"isQuery\":\"1\",\"isRequired\":\"0\",\"javaField\":\"deviceName\",\"javaType\":\"String\",\"list\":true,\"params\":{},\"pk\":false,\"query\":true,\"queryType\":\"LIKE\",\"required\":false,\"sort\":3,\"superColumn\":false,\"tableId\":1,\"updateBy\":\"\",\"updateTime\":\"2025-08-29 10:05:06\",\"usableColumn\":false},{\"capJavaField\":\"NoteName\",\"columnComment\":\"备注名称\",\"columnId\":4,\"columnName\":\"note_name\",\"columnType\":\"varchar(100)\",\"createBy\":\"admin\",\"createTime\":\"2025-08-22 14:26:37\",\"dictType\":\"\",\"edit\":true,\"htmlType\":\"input\",\"increment\":false,\"insert\":true,\"isEdit\":\"1\",\"isIncrement\":\"0\"','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-08-29 10:05:17',36),(21,'代码生成',8,'com.storm.gen.controller.GenController.batchGenCode()','GET',1,'admin',NULL,'/gen/batchGenCode','127.0.0.1','','{\"tables\":\"device\"}',NULL,0,NULL,'2025-08-29 10:05:20',285),(22,'角色管理',2,'com.storm.system.controller.SysRoleController.edit()','PUT',1,'admin',NULL,'/role','127.0.0.1','','{\"admin\":false,\"createTime\":\"2025-08-20 10:54:10\",\"dataScope\":\"2\",\"delFlag\":\"0\",\"deptCheckStrictly\":true,\"flag\":false,\"menuCheckStrictly\":true,\"menuIds\":[1075,1076,1077,1078,1079,1080,1081],\"params\":{},\"remark\":\"普通角色\",\"roleId\":2,\"roleKey\":\"common\",\"roleName\":\"普通角色\",\"roleSort\":2,\"status\":\"0\",\"updateBy\":\"admin\"}','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-09-03 18:02:10',91),(23,'用户管理',1,'com.storm.system.controller.SysUserController.add()','POST',1,'admin',NULL,'/user','127.0.0.1','','{\"admin\":false,\"createBy\":\"admin\",\"nickName\":\"user\",\"params\":{},\"postIds\":[],\"roleIds\":[2],\"status\":\"0\",\"userId\":3,\"userName\":\"user\"}','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-09-03 18:03:01',128),(24,'个人信息',2,'com.storm.system.controller.SysProfileController.updatePwd()','PUT',1,'user',NULL,'/user/profile/updatePwd','127.0.0.1','','{}','{\"msg\":\"新密码不能与旧密码相同\",\"code\":500}',0,NULL,'2025-09-03 20:19:26',129),(25,'个人信息',2,'com.storm.system.controller.SysProfileController.updatePwd()','PUT',1,'user',NULL,'/user/profile/updatePwd','127.0.0.1','','{}','{\"msg\":\"操作成功\",\"code\":200}',0,NULL,'2025-09-03 20:20:34',325); +/*!40000 ALTER TABLE `sys_oper_log` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_post` +-- + +DROP TABLE IF EXISTS `sys_post`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_post` ( + `post_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '岗位ID', + `post_code` varchar(64) NOT NULL COMMENT '岗位编码', + `post_name` varchar(50) NOT NULL COMMENT '岗位名称', + `post_sort` int(4) NOT NULL COMMENT '显示顺序', + `status` char(1) NOT NULL COMMENT '状态(0正常 1停用)', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`post_id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='岗位信息表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_post` +-- + +LOCK TABLES `sys_post` WRITE; +/*!40000 ALTER TABLE `sys_post` DISABLE KEYS */; +INSERT INTO `sys_post` VALUES (1,'ceo','董事长',1,'0','admin','2025-08-20 10:54:10','',NULL,''),(2,'se','项目经理',2,'0','admin','2025-08-20 10:54:10','',NULL,''),(3,'hr','人力资源',3,'0','admin','2025-08-20 10:54:10','',NULL,''),(4,'user','普通员工',4,'0','admin','2025-08-20 10:54:10','',NULL,''); +/*!40000 ALTER TABLE `sys_post` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_role` +-- + +DROP TABLE IF EXISTS `sys_role`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_role` ( + `role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID', + `role_name` varchar(30) NOT NULL COMMENT '角色名称', + `role_key` varchar(100) NOT NULL COMMENT '角色权限字符串', + `role_sort` int(4) NOT NULL COMMENT '显示顺序', + `data_scope` char(1) DEFAULT '1' COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)', + `menu_check_strictly` tinyint(1) DEFAULT '1' COMMENT '菜单树选择项是否关联显示', + `dept_check_strictly` tinyint(1) DEFAULT '1' COMMENT '部门树选择项是否关联显示', + `status` char(1) NOT NULL COMMENT '角色状态(0正常 1停用)', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`role_id`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色信息表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_role` +-- + +LOCK TABLES `sys_role` WRITE; +/*!40000 ALTER TABLE `sys_role` DISABLE KEYS */; +INSERT INTO `sys_role` VALUES (1,'超级管理员','admin',1,'1',1,1,'0','0','admin','2025-08-20 10:54:10','',NULL,'超级管理员'),(2,'普通角色','common',2,'2',1,1,'0','0','admin','2025-08-20 10:54:10','admin','2025-09-03 18:02:10','普通角色'); +/*!40000 ALTER TABLE `sys_role` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_role_dept` +-- + +DROP TABLE IF EXISTS `sys_role_dept`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_role_dept` ( + `role_id` bigint(20) NOT NULL COMMENT '角色ID', + `dept_id` bigint(20) NOT NULL COMMENT '部门ID', + PRIMARY KEY (`role_id`,`dept_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色和部门关联表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_role_dept` +-- + +LOCK TABLES `sys_role_dept` WRITE; +/*!40000 ALTER TABLE `sys_role_dept` DISABLE KEYS */; +INSERT INTO `sys_role_dept` VALUES (2,100),(2,101),(2,105); +/*!40000 ALTER TABLE `sys_role_dept` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_role_menu` +-- + +DROP TABLE IF EXISTS `sys_role_menu`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_role_menu` ( + `role_id` bigint(20) NOT NULL COMMENT '角色ID', + `menu_id` bigint(20) NOT NULL COMMENT '菜单ID', + PRIMARY KEY (`role_id`,`menu_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色和菜单关联表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_role_menu` +-- + +LOCK TABLES `sys_role_menu` WRITE; +/*!40000 ALTER TABLE `sys_role_menu` DISABLE KEYS */; +INSERT INTO `sys_role_menu` VALUES (2,1075),(2,1076),(2,1077),(2,1078),(2,1079),(2,1080),(2,1081); +/*!40000 ALTER TABLE `sys_role_menu` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_user` +-- + +DROP TABLE IF EXISTS `sys_user`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_user` ( + `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID', + `user_name` varchar(30) NOT NULL COMMENT '用户账号', + `nick_name` varchar(30) NOT NULL COMMENT '用户昵称', + `user_type` varchar(2) DEFAULT '00' COMMENT '用户类型(00系统用户)', + `email` varchar(50) DEFAULT '' COMMENT '用户邮箱', + `phonenumber` varchar(11) DEFAULT '' COMMENT '手机号码', + `sex` char(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)', + `avatar` varchar(100) DEFAULT '' COMMENT '头像地址', + `password` varchar(100) DEFAULT '' COMMENT '密码', + `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + `login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP', + `login_date` datetime DEFAULT NULL COMMENT '最后登录时间', + `pwd_update_date` datetime DEFAULT NULL COMMENT '密码最后更新时间', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_user` +-- + +LOCK TABLES `sys_user` WRITE; +/*!40000 ALTER TABLE `sys_user` DISABLE KEYS */; +INSERT INTO `sys_user` VALUES (1,103,'admin','admin','00','admin@163.com','15888888888','1','','$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2','0','0','127.0.0.1','2025-09-03 20:23:02','2025-08-20 10:54:10','admin','2025-08-20 10:54:10','','2025-09-03 20:23:00','管理员'),(2,105,'dz','丁真','00','dz@qq.com','15666666666','1','','$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2','0','0','127.0.0.1','2025-09-03 18:01:14','2025-08-20 10:54:10','admin','2025-08-20 10:54:10','','2025-09-03 18:01:12','测试员'),(3,NULL,'user','user','00','','','0','','$2a$10$5n5lgBI34.zDa.7aqwuYjesCZE9thpnMlXynmudHprba.ucMSW9XC','0','0','127.0.0.1','2025-09-03 20:17:43','2025-09-03 20:20:34','admin','2025-09-03 18:03:01','','2025-09-03 20:17:41',NULL); +/*!40000 ALTER TABLE `sys_user` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_user_post` +-- + +DROP TABLE IF EXISTS `sys_user_post`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_user_post` ( + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `post_id` bigint(20) NOT NULL COMMENT '岗位ID', + PRIMARY KEY (`user_id`,`post_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户与岗位关联表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_user_post` +-- + +LOCK TABLES `sys_user_post` WRITE; +/*!40000 ALTER TABLE `sys_user_post` DISABLE KEYS */; +INSERT INTO `sys_user_post` VALUES (1,1),(2,2); +/*!40000 ALTER TABLE `sys_user_post` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sys_user_role` +-- + +DROP TABLE IF EXISTS `sys_user_role`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sys_user_role` ( + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `role_id` bigint(20) NOT NULL COMMENT '角色ID', + PRIMARY KEY (`user_id`,`role_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户和角色关联表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sys_user_role` +-- + +LOCK TABLES `sys_user_role` WRITE; +/*!40000 ALTER TABLE `sys_user_role` DISABLE KEYS */; +INSERT INTO `sys_user_role` VALUES (1,1),(2,2),(3,2); +/*!40000 ALTER TABLE `sys_user_role` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping routines for database 'storm' +-- +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-03 21:28:28 diff --git a/docker/mysql/db/dump-storm-config-202509041019.sql b/docker/mysql/db/dump-storm-config-202509041019.sql new file mode 100644 index 0000000..c67f1e8 --- /dev/null +++ b/docker/mysql/db/dump-storm-config-202509041019.sql @@ -0,0 +1,450 @@ +-- MySQL dump 10.13 Distrib 8.0.42, for Win64 (x86_64) +-- +-- Host: 192.168.48.129 Database: storm-config +-- ------------------------------------------------------ +-- Server version 5.7.44 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `config_info` +-- + +USE `storm-config`; + +DROP TABLE IF EXISTS `config_info`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `config_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id', + `group_id` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'group_id', + `content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content', + `md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COLLATE utf8_bin COMMENT 'source user', + `src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip', + `app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name', + `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段', + `c_desc` varchar(256) COLLATE utf8_bin DEFAULT NULL COMMENT 'configuration description', + `c_use` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT 'configuration usage', + `effect` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT '配置生效的描述', + `type` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT '配置的类型', + `c_schema` text COLLATE utf8_bin COMMENT '配置的模式', + `encrypted_data_key` varchar(1024) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT '密钥', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) +) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `config_info` +-- + +LOCK TABLES `config_info` WRITE; +/*!40000 ALTER TABLE `config_info` DISABLE KEYS */; +INSERT INTO `config_info` VALUES (1,'application-dev.yml','DEFAULT_GROUP','spring:\n autoconfigure:\n exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\n\n# feign 配置\nfeign:\n sentinel:\n enabled: true\n okhttp:\n enabled: true\n httpclient:\n enabled: false\n client:\n config:\n default:\n connectTimeout: 10000\n readTimeout: 10000\n compression:\n request:\n enabled: true\n min-request-size: 8192\n response:\n enabled: true\n\n# 暴露监控端点\nmanagement:\n endpoints:\n web:\n exposure:\n include: \'*\'\n','9928f41dfb10386ad38b3254af5692e0','2020-05-20 12:00:00','2024-08-29 12:14:45','nacos','0:0:0:0:0:0:0:1','','','通用配置','null','null','yaml','',''),(2,'storm-gateway-dev.yml','DEFAULT_GROUP','spring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n cloud:\n gateway:\n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: storm-auth\n uri: lb://storm-auth\n predicates:\n - Path=/auth/**\n filters:\n # 验证码处理\n - CacheRequestBody\n - ValidateCodeFilter\n - StripPrefix=1\n # 代码生成\n - id: storm-gen\n uri: lb://storm-gen\n predicates:\n - Path=/code/**\n filters:\n - StripPrefix=1\n # 定时任务\n - id: storm-job\n uri: lb://storm-job\n predicates:\n - Path=/schedule/**\n filters:\n - StripPrefix=1\n # 系统模块\n - id: storm-system\n uri: lb://storm-system\n predicates:\n - Path=/system/**\n filters:\n - StripPrefix=1\n # 文件服务\n - id: storm-file\n uri: lb://storm-file\n predicates:\n - Path=/file/**\n filters:\n - StripPrefix=1\n #设备管理\n - id: storm-device\n uri: lb://storm-device\n predicates:\n - Path=/device/**\n filters:\n - StripPrefix=1\n\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n - /system/notice\n\n # 不校验白名单\n ignore:\n whites:\n - /auth/logout\n - /auth/login\n - /auth/register\n - /*/v2/api-docs\n - /*/v3/api-docs\n - /csrf\n\n# springdoc配置\nspringdoc:\n webjars:\n # 访问前缀\n prefix:\n# knife4j配置\nknife4j:\n enable: true # 启用Knife4j\n gateway:\n enabled: true # 启用网关聚合功能\n strategy: discover # 使用服务发现模式进行聚合\n discover:\n enabled: true # 启用服务发现\n version: openapi3 # 指定OpenAPI版本,与你使用的规范一致\n all-services: true # 聚合所有服务\n group-name: default # 默认分组名称','0837a3c65c0c2216f75951bb02095ff5','2020-05-14 14:17:55','2025-09-03 12:29:11','nacos','192.168.48.1','','','网关模块','null','null','yaml','',''),(3,'storm-auth-dev.yml','DEFAULT_GROUP','spring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n','9fe68aa29b8c34380d6b0ebfbca92d25','2020-11-20 00:00:00','2025-09-03 12:29:38','nacos','192.168.48.1','','','认证中心','null','null','yaml','',''),(4,'storm-monitor-dev.yml','DEFAULT_GROUP','# spring\nspring:\n security:\n user:\n name: storm\n password: 123456\n boot:\n admin:\n ui:\n title: 具身风暴服务状态监控\n','6f122fd2bfb8d45f858e7d6529a9cd44','2020-11-20 00:00:00','2024-08-29 12:15:11','nacos','0:0:0:0:0:0:0:1','','','监控中心','null','null','yaml','',''),(5,'storm-system-dev.yml','DEFAULT_GROUP','# spring配置\nspring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: storm\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n connectTimeout: 30000\n socketTimeout: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://storm-mysql:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: 123456\n # url: \n # driver-class-name: \n\n# springdoc配置\n#springdoc:\n# gatewayUrl: http://localhost:8080/${spring.application.name}\n# api-docs:\n # 是否开启接口文档\n# enabled: true\n# info:\n # 标题\n# title: \'系统模块接口文档\'\n # 描述\n# description: \'系统模块接口描述\'\n # 作者信息\n# contact:\n# name: storm\n# url: https://storm.vip\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.system.controller\n title: 具身风暴 - 系统模块接口文档\n description: 该服务包含系统配置有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html','dd261eb4c0c11f82cd9156897690f945','2020-11-20 00:00:00','2025-09-03 12:30:53','nacos','192.168.48.1','','','系统模块','null','null','yaml','',''),(6,'storm-gen-dev.yml','DEFAULT_GROUP','# spring配置\nspring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://storm-mysql:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.gen.controller\n title: 具身风暴 - 代码生成接口文档\n description: 该服务包含代码生成有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html\n\n# 代码生成\ngen:\n # 作者\n author: storm\n # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool\n packageName: com.storm.system\n # 自动去除表前缀,默认是false\n autoRemovePre: false\n # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)\n tablePrefix: sys_\n # 是否允许生成文件覆盖到本地(自定义路径),默认不允许\n allowOverwrite: false','8f89b0bc11a538d015fbe824fbc39372','2020-11-20 00:00:00','2025-09-03 12:31:38','nacos','192.168.48.1','','','代码生成','null','null','yaml','',''),(7,'storm-job-dev.yml','DEFAULT_GROUP','# spring配置\nspring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://storm-mysql:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.job.controller\n title: 具身风暴 - 定时任务接口文档\n description: 该服务包含定时任务有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html','86b30a714748a626caccbfa797660797','2020-11-20 00:00:00','2025-09-03 12:32:18','nacos','192.168.48.1','','','定时任务','null','null','yaml','',''),(8,'storm-file-dev.yml','DEFAULT_GROUP','# 本地文件上传 \nfile:\n domain: http://127.0.0.1:9300\n path: D:/storm/uploadPath\n prefix: /statics\n\n# FastDFS配置\nfdfs:\n domain: http://8.129.231.12\n soTimeout: 3000\n connectTimeout: 2000\n trackerList: 8.129.231.12:22122\n\n# Minio配置\nminio:\n url: http://8.129.231.12:9000\n accessKey: minioadmin\n secretKey: minioadmin\n bucketName: test\n# 华为云obs\nstorm:\n huaweiobs:\n ENDPOINT: \"obs.cn-south-1.myhuaweicloud.com\"\n CLOUD_SDK_AK: \"OD4WEG0X1TYXQUSLM5LM\"\n CLOUD_SDK_SK: \"QQTf5rBpvdnq1qf1OEj0VFHinFQIqW7vPN0N8vpN\"\n bucketName: \"robotstorm-app\"','5439696a0662c2ab4cf42081ed0798b5','2020-11-20 00:00:00','2025-08-25 08:34:17','nacos','192.168.48.1','','','文件服务','null','null','yaml','',''),(9,'sentinel-storm-gateway','DEFAULT_GROUP','[\r\n {\r\n \"resource\": \"storm-auth\",\r\n \"count\": 500,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n },\r\n {\r\n \"resource\": \"storm-system\",\r\n \"count\": 1000,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n },\r\n {\r\n \"resource\": \"storm-gen\",\r\n \"count\": 200,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n },\r\n {\r\n \"resource\": \"storm-job\",\r\n \"count\": 300,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n }\r\n]','9f3a3069261598f74220bc47958ec252','2020-11-20 00:00:00','2020-11-20 00:00:00',NULL,'0:0:0:0:0:0:0:1','','','限流策略','null','null','json',NULL,''),(10,'storm-device-dev.yml','DEFAULT_GROUP','# spring配置\nspring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://storm-mysql:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html\n','87c3c7a5f2c446ee97be7a52e98672ee','2025-08-22 09:52:28','2025-09-03 12:33:00','nacos','192.168.48.1','','','','','','yaml','',''); +/*!40000 ALTER TABLE `config_info` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `config_info_aggr` +-- + +DROP TABLE IF EXISTS `config_info_aggr`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `config_info_aggr` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id', + `group_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'group_id', + `datum_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'datum_id', + `content` longtext COLLATE utf8_bin NOT NULL COMMENT '内容', + `gmt_modified` datetime NOT NULL COMMENT '修改时间', + `app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL, + `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `config_info_aggr` +-- + +LOCK TABLES `config_info_aggr` WRITE; +/*!40000 ALTER TABLE `config_info_aggr` DISABLE KEYS */; +/*!40000 ALTER TABLE `config_info_aggr` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `config_info_beta` +-- + +DROP TABLE IF EXISTS `config_info_beta`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `config_info_beta` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id', + `group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id', + `app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name', + `content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content', + `beta_ips` varchar(1024) COLLATE utf8_bin DEFAULT NULL COMMENT 'betaIps', + `md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COLLATE utf8_bin COMMENT 'source user', + `src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip', + `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段', + `encrypted_data_key` varchar(1024) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT '密钥', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `config_info_beta` +-- + +LOCK TABLES `config_info_beta` WRITE; +/*!40000 ALTER TABLE `config_info_beta` DISABLE KEYS */; +/*!40000 ALTER TABLE `config_info_beta` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `config_info_gray` +-- + +DROP TABLE IF EXISTS `config_info_gray`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `config_info_gray` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `src_user` text COMMENT 'src_user', + `src_ip` varchar(100) DEFAULT NULL COMMENT 'src_ip', + `gmt_create` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_create', + `gmt_modified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_modified', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', + `gray_name` varchar(128) NOT NULL COMMENT 'gray_name', + `gray_rule` text NOT NULL COMMENT 'gray_rule', + `encrypted_data_key` varchar(256) NOT NULL DEFAULT '' COMMENT 'encrypted_data_key', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfogray_datagrouptenantgray` (`data_id`,`group_id`,`tenant_id`,`gray_name`), + KEY `idx_dataid_gmt_modified` (`data_id`,`gmt_modified`), + KEY `idx_gmt_modified` (`gmt_modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='config_info_gray'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `config_info_gray` +-- + +LOCK TABLES `config_info_gray` WRITE; +/*!40000 ALTER TABLE `config_info_gray` DISABLE KEYS */; +/*!40000 ALTER TABLE `config_info_gray` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `config_info_tag` +-- + +DROP TABLE IF EXISTS `config_info_tag`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `config_info_tag` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id', + `group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id', + `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id', + `tag_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'tag_id', + `app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name', + `content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content', + `md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COLLATE utf8_bin COMMENT 'source user', + `src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `config_info_tag` +-- + +LOCK TABLES `config_info_tag` WRITE; +/*!40000 ALTER TABLE `config_info_tag` DISABLE KEYS */; +/*!40000 ALTER TABLE `config_info_tag` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `config_tags_relation` +-- + +DROP TABLE IF EXISTS `config_tags_relation`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `config_tags_relation` ( + `id` bigint(20) NOT NULL COMMENT 'id', + `tag_name` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'tag_name', + `tag_type` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT 'tag_type', + `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id', + `group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id', + `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id', + `nid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识', + PRIMARY KEY (`nid`), + UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), + KEY `idx_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `config_tags_relation` +-- + +LOCK TABLES `config_tags_relation` WRITE; +/*!40000 ALTER TABLE `config_tags_relation` DISABLE KEYS */; +/*!40000 ALTER TABLE `config_tags_relation` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `group_capacity` +-- + +DROP TABLE IF EXISTS `group_capacity`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `group_capacity` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `group_id` varchar(128) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群', + `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', + `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', + `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', + `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值', + `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', + `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_group_id` (`group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `group_capacity` +-- + +LOCK TABLES `group_capacity` WRITE; +/*!40000 ALTER TABLE `group_capacity` DISABLE KEYS */; +/*!40000 ALTER TABLE `group_capacity` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `his_config_info` +-- + +DROP TABLE IF EXISTS `his_config_info`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `his_config_info` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'id', + `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识', + `data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id', + `group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id', + `app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name', + `content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content', + `md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COLLATE utf8_bin COMMENT 'source user', + `src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip', + `op_type` char(10) COLLATE utf8_bin DEFAULT NULL COMMENT 'operation type', + `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段', + `encrypted_data_key` varchar(1024) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT '密钥', + `publish_type` varchar(50) COLLATE utf8_bin DEFAULT 'formal' COMMENT 'publish type gray or formal', + `gray_name` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'gray name', + `ext_info` longtext COLLATE utf8_bin COMMENT 'ext info', + PRIMARY KEY (`nid`), + KEY `idx_gmt_create` (`gmt_create`), + KEY `idx_gmt_modified` (`gmt_modified`), + KEY `idx_did` (`data_id`) +) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `his_config_info` +-- + +LOCK TABLES `his_config_info` WRITE; +/*!40000 ALTER TABLE `his_config_info` DISABLE KEYS */; +INSERT INTO `his_config_info` VALUES (2,1,'storm-gateway-dev.yml','DEFAULT_GROUP','','spring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n cloud:\n gateway:\n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: storm-auth\n uri: lb://storm-auth\n predicates:\n - Path=/auth/**\n filters:\n # 验证码处理\n - CacheRequestBody\n - ValidateCodeFilter\n - StripPrefix=1\n # 代码生成\n - id: storm-gen\n uri: lb://storm-gen\n predicates:\n - Path=/code/**\n filters:\n - StripPrefix=1\n # 定时任务\n - id: storm-job\n uri: lb://storm-job\n predicates:\n - Path=/schedule/**\n filters:\n - StripPrefix=1\n # 系统模块\n - id: storm-system\n uri: lb://storm-system\n predicates:\n - Path=/system/**\n filters:\n - StripPrefix=1\n # 文件服务\n - id: storm-file\n uri: lb://storm-file\n predicates:\n - Path=/file/**\n filters:\n - StripPrefix=1\n\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n - /system/notice\n\n # 不校验白名单\n ignore:\n whites:\n - /auth/logout\n - /auth/login\n - /auth/register\n - /*/v2/api-docs\n - /*/v3/api-docs\n - /csrf\n\n# springdoc配置\nspringdoc:\n webjars:\n # 访问前缀\n prefix:\n','5af70e741c0c90d5afe12ca4a116150e','2025-08-20 19:05:12','2025-08-20 11:05:12','nacos','192.168.48.1','U','','','formal',NULL,NULL),(2,2,'storm-gateway-dev.yml','DEFAULT_GROUP','','spring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n cloud:\n gateway:\n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: storm-auth\n uri: lb://storm-auth\n predicates:\n - Path=/auth/**\n filters:\n # 验证码处理\n - CacheRequestBody\n - ValidateCodeFilter\n - StripPrefix=1\n # 代码生成\n - id: storm-gen\n uri: lb://storm-gen\n predicates:\n - Path=/code/**\n filters:\n - StripPrefix=1\n # 定时任务\n - id: storm-job\n uri: lb://storm-job\n predicates:\n - Path=/schedule/**\n filters:\n - StripPrefix=1\n # 系统模块\n - id: storm-system\n uri: lb://storm-system\n predicates:\n - Path=/system/**\n filters:\n - StripPrefix=1\n # 文件服务\n - id: storm-file\n uri: lb://storm-file\n predicates:\n - Path=/file/**\n filters:\n - StripPrefix=1\n\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n - /system/notice\n\n # 不校验白名单\n ignore:\n whites:\n - /auth/logout\n - /auth/login\n - /auth/register\n - /*/v2/api-docs\n - /*/v3/api-docs\n - /csrf\n\n# springdoc配置\nspringdoc:\n webjars:\n # 访问前缀\n prefix:\n','5af70e741c0c90d5afe12ca4a116150e','2025-08-21 09:56:01','2025-08-21 01:56:02','nacos','192.168.48.1','U','','','formal',NULL,NULL),(3,3,'storm-auth-dev.yml','DEFAULT_GROUP','','spring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n','9fe68aa29b8c34380d6b0ebfbca92d25','2025-08-21 09:56:18','2025-08-21 01:56:18','nacos','192.168.48.1','U','','','formal',NULL,NULL),(5,4,'storm-system-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: storm\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n connectTimeout: 30000\n socketTimeout: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://storm-mysql:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: 123456\n # url: \n # driver-class-name: \n\n# mybatis配置\nmybatis:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.system\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath:mapper/**/*.xml\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'系统模块接口文档\'\n # 描述\n description: \'系统模块接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n','c21b79298a7cce85085960a7de58e207','2025-08-21 09:57:02','2025-08-21 01:57:02','nacos','192.168.48.1','U','','','formal',NULL,NULL),(6,5,'storm-gen-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://storm-mysql:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# mybatis配置\nmybatis:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.gen.domain\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath:mapper/**/*.xml\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'代码生成接口文档\'\n # 描述\n description: \'代码生成接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n\n# 代码生成\ngen:\n # 作者\n author: storm\n # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool\n packageName: com.storm.system\n # 自动去除表前缀,默认是false\n autoRemovePre: false\n # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)\n tablePrefix: sys_\n # 是否允许生成文件覆盖到本地(自定义路径),默认不允许\n allowOverwrite: false','0b2e7aa209e8e02a2cbfa3ca4e50b754','2025-08-21 09:57:21','2025-08-21 01:57:22','nacos','192.168.48.1','U','','','formal',NULL,NULL),(7,6,'storm-job-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: storm-redis\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://storm-mysql:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# mybatis配置\nmybatis:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.job.domain\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath:mapper/**/*.xml\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'定时任务接口文档\'\n # 描述\n description: \'定时任务接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n','e0b99c1387748bb547a54d4cb913bd90','2025-08-21 09:57:40','2025-08-21 01:57:41','nacos','192.168.48.1','U','','','formal',NULL,NULL),(6,7,'storm-gen-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# mybatis配置\nmybatis:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.gen.domain\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath:mapper/**/*.xml\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'代码生成接口文档\'\n # 描述\n description: \'代码生成接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n\n# 代码生成\ngen:\n # 作者\n author: storm\n # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool\n packageName: com.storm.system\n # 自动去除表前缀,默认是false\n autoRemovePre: false\n # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)\n tablePrefix: sys_\n # 是否允许生成文件覆盖到本地(自定义路径),默认不允许\n allowOverwrite: false','b027bd40a28ad159204586894c6c8227','2025-08-22 13:57:23','2025-08-22 05:57:23','nacos','192.168.48.1','U','','','formal',NULL,NULL),(6,8,'storm-gen-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# mybatis-plus配置\nmybatis-plus:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.gen.domain\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath:mapper/**/*Mapper.xml\n # 全局配置\n global-config:\n db-config:\n id-type: auto #id生成策略为自增\n configuration: \n map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'代码生成接口文档\'\n # 描述\n description: \'代码生成接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n\n# 代码生成\ngen:\n # 作者\n author: storm\n # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool\n packageName: com.storm.system\n # 自动去除表前缀,默认是false\n autoRemovePre: false\n # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)\n tablePrefix: sys_\n # 是否允许生成文件覆盖到本地(自定义路径),默认不允许\n allowOverwrite: false','d4552a61b987aefb66e4f0023cd5e3ef','2025-08-22 14:06:34','2025-08-22 06:06:34','nacos','192.168.48.1','U','','','formal',NULL,NULL),(7,9,'storm-job-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# mybatis配置\nmybatis:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.job.domain\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath:mapper/**/*.xml\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'定时任务接口文档\'\n # 描述\n description: \'定时任务接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n','a30949fad7c05e0f51e38f12eda88541','2025-08-22 14:09:18','2025-08-22 06:09:18','nacos','192.168.48.1','U','','','formal',NULL,NULL),(5,10,'storm-system-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: storm\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n connectTimeout: 30000\n socketTimeout: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: 123456\n # url: \n # driver-class-name: \n\n# mybatis配置\nmybatis:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.system\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath:mapper/**/*.xml\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'系统模块接口文档\'\n # 描述\n description: \'系统模块接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n','ab71e1848e51f9dcf7ef1b32dceb1424','2025-08-22 14:10:49','2025-08-22 06:10:50','nacos','192.168.48.1','U','','','formal',NULL,NULL),(0,11,'storm-device-dev.yml','DEFAULT_GROUP','','# MyBatisPlus配置\r\nmybatis-plus:\r\n # 搜索指定包别名\r\n typeAliasesPackage: com.storm.**.domain\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapperLocations: classpath*:mapper/**/*Mapper.xml\r\n # 全局配置\r\n global-config:\r\n db-config:\r\n id-type: auto #id生成策略为自增\r\n configuration:\r\n map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名','8010a968db18b919e7362adf1adc5975','2025-08-22 17:52:28','2025-08-22 09:52:28','nacos','192.168.48.1','I','','','formal',NULL,NULL),(10,12,'storm-device-dev.yml','DEFAULT_GROUP','','# MyBatisPlus配置\r\nmybatis-plus:\r\n # 搜索指定包别名\r\n typeAliasesPackage: com.storm.**.domain\r\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\r\n mapperLocations: classpath*:mapper/**/*Mapper.xml\r\n # 全局配置\r\n global-config:\r\n db-config:\r\n id-type: auto #id生成策略为自增\r\n configuration:\r\n map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名','8010a968db18b919e7362adf1adc5975','2025-08-22 17:55:03','2025-08-22 09:55:04','nacos','192.168.48.1','U','','','formal',NULL,NULL),(2,13,'storm-gateway-dev.yml','DEFAULT_GROUP','','spring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n cloud:\n gateway:\n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: storm-auth\n uri: lb://storm-auth\n predicates:\n - Path=/auth/**\n filters:\n # 验证码处理\n - CacheRequestBody\n - ValidateCodeFilter\n - StripPrefix=1\n # 代码生成\n - id: storm-gen\n uri: lb://storm-gen\n predicates:\n - Path=/code/**\n filters:\n - StripPrefix=1\n # 定时任务\n - id: storm-job\n uri: lb://storm-job\n predicates:\n - Path=/schedule/**\n filters:\n - StripPrefix=1\n # 系统模块\n - id: storm-system\n uri: lb://storm-system\n predicates:\n - Path=/system/**\n filters:\n - StripPrefix=1\n # 文件服务\n - id: storm-file\n uri: lb://storm-file\n predicates:\n - Path=/file/**\n filters:\n - StripPrefix=1\n\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n - /system/notice\n\n # 不校验白名单\n ignore:\n whites:\n - /auth/logout\n - /auth/login\n - /auth/register\n - /*/v2/api-docs\n - /*/v3/api-docs\n - /csrf\n\n# springdoc配置\nspringdoc:\n webjars:\n # 访问前缀\n prefix:\n','0103e3155fa174e3f4f1eebbcbda1eb3','2025-08-22 19:04:54','2025-08-22 11:04:54','nacos','192.168.48.1','U','','','formal',NULL,NULL),(8,14,'storm-file-dev.yml','DEFAULT_GROUP','','# 本地文件上传 \r\nfile:\r\n domain: http://127.0.0.1:9300\r\n path: D:/storm/uploadPath\r\n prefix: /statics\r\n\r\n# FastDFS配置\r\nfdfs:\r\n domain: http://8.129.231.12\r\n soTimeout: 3000\r\n connectTimeout: 2000\r\n trackerList: 8.129.231.12:22122\r\n\r\n# Minio配置\r\nminio:\r\n url: http://8.129.231.12:9000\r\n accessKey: minioadmin\r\n secretKey: minioadmin\r\n bucketName: test','c26c0e6757a2e312f2c9621f1e36b6b0','2025-08-25 16:34:17','2025-08-25 08:34:17','nacos','192.168.48.1','U','','','formal',NULL,NULL),(5,15,'storm-system-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: storm\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n connectTimeout: 30000\n socketTimeout: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: 123456\n # url: \n # driver-class-name: \n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'系统模块接口文档\'\n # 描述\n description: \'系统模块接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n','ba6bea10e9e88ebaebe3216819d54bf2','2025-08-26 11:15:01','2025-08-26 03:15:01','nacos','192.168.48.1','U','','','formal',NULL,NULL),(6,16,'storm-gen-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'代码生成接口文档\'\n # 描述\n description: \'代码生成接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n\n# 代码生成\ngen:\n # 作者\n author: storm\n # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool\n packageName: com.storm.system\n # 自动去除表前缀,默认是false\n autoRemovePre: false\n # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)\n tablePrefix: sys_\n # 是否允许生成文件覆盖到本地(自定义路径),默认不允许\n allowOverwrite: false','3cdfe7ab0fde63b063e932e57048bb20','2025-08-26 11:16:19','2025-08-26 03:16:20','nacos','192.168.48.1','U','','','formal',NULL,NULL),(7,17,'storm-job-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'定时任务接口文档\'\n # 描述\n description: \'定时任务接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n','a8fc8ba80070c0a11c4c97fa43bc8bf4','2025-08-26 11:17:33','2025-08-26 03:17:34','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,18,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'设备管理接口文档\'\n # 描述\n description: \'设备管理接口描述\'\n # 作者信息\n contact:\n name: storm\n url: https://storm.vip\n','97c788f0bcef89a7c2efd1f7f01f72f3','2025-08-26 11:18:48','2025-08-26 03:18:49','nacos','192.168.48.1','U','','','formal',NULL,NULL),(5,19,'storm-system-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: storm\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n connectTimeout: 30000\n socketTimeout: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: 123456\n # url: \n # driver-class-name: \n\n# springdoc配置\n#springdoc:\n# gatewayUrl: http://localhost:8080/${spring.application.name}\n# api-docs:\n # 是否开启接口文档\n# enabled: true\n# info:\n # 标题\n# title: \'系统模块接口文档\'\n # 描述\n# description: \'系统模块接口描述\'\n # 作者信息\n# contact:\n# name: storm\n# url: https://storm.vip\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n package-path: com.storm.system.controller\n title: 具身风暴 - 系统模块接口文档\n description: 该服务包含系统配置有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0','f92b2ee13de4d11d01e6262af180e08f','2025-08-26 14:00:43','2025-08-26 06:00:43','nacos','192.168.48.1','U','','','formal',NULL,NULL),(6,20,'storm-gen-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n package-path: com.storm.gen.controller\n title: 具身风暴 - 代码生成接口文档\n description: 该服务包含代码生成有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n\n# 代码生成\ngen:\n # 作者\n author: storm\n # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool\n packageName: com.storm.system\n # 自动去除表前缀,默认是false\n autoRemovePre: false\n # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)\n tablePrefix: sys_\n # 是否允许生成文件覆盖到本地(自定义路径),默认不允许\n allowOverwrite: false','8b697ef4f3abc28e7017733be470a1d7','2025-08-26 14:01:28','2025-08-26 06:01:28','nacos','192.168.48.1','U','','','formal',NULL,NULL),(7,21,'storm-job-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n package-path: com.storm.job.controller\n title: 具身风暴 - 定时任务接口文档\n description: 该服务包含定时任务有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0','d13c9b19a1cf2991667f264c54be1cfb','2025-08-26 14:01:39','2025-08-26 06:01:40','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,22,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0','c5e538466ede452873c58e058f3a4d49','2025-08-26 14:02:01','2025-08-26 06:02:01','nacos','192.168.48.1','U','','','formal',NULL,NULL),(2,23,'storm-gateway-dev.yml','DEFAULT_GROUP','','spring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n cloud:\n gateway:\n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: storm-auth\n uri: lb://storm-auth\n predicates:\n - Path=/auth/**\n filters:\n # 验证码处理\n - CacheRequestBody\n - ValidateCodeFilter\n - StripPrefix=1\n # 代码生成\n - id: storm-gen\n uri: lb://storm-gen\n predicates:\n - Path=/code/**\n filters:\n - StripPrefix=1\n # 定时任务\n - id: storm-job\n uri: lb://storm-job\n predicates:\n - Path=/schedule/**\n filters:\n - StripPrefix=1\n # 系统模块\n - id: storm-system\n uri: lb://storm-system\n predicates:\n - Path=/system/**\n filters:\n - StripPrefix=1\n # 文件服务\n - id: storm-file\n uri: lb://storm-file\n predicates:\n - Path=/file/**\n filters:\n - StripPrefix=1\n #设备管理\n - id: storm-device\n uri: lb://storm-device\n predicates:\n - Path=/device/**\n filters:\n - StripPrefix=1\n\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n - /system/notice\n\n # 不校验白名单\n ignore:\n whites:\n - /auth/logout\n - /auth/login\n - /auth/register\n - /*/v2/api-docs\n - /*/v3/api-docs\n - /csrf\n\n# springdoc配置\nspringdoc:\n webjars:\n # 访问前缀\n prefix:\n','0d8dd843213ec7c4dd635fbb794cf785','2025-08-26 14:41:46','2025-08-26 06:41:46','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,24,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0','40d0723bd05691feca71985c5bb3fe22','2025-08-26 15:09:37','2025-08-26 07:09:37','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,25,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html\n enabled: false # 禁用默认的Swagger UI,使用Knife4j的UI','f7df913dc40d5e6e8d9c96e4d39cac51','2025-08-26 16:09:19','2025-08-26 08:09:19','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,26,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n# package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html\n enabled: false # 禁用默认的Swagger UI,使用Knife4j的UI\n group-configs:\n - group: \'default\'\n paths-to-match: \'/**\'\n packages-to-scan: \n - com.storm.device.controller','539ba97226dc0ba4e96a4d783231f28a','2025-08-26 16:17:30','2025-08-26 08:17:31','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,27,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n# package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html\n enabled: false # 禁用默认的Swagger UI,使用Knife4j的UI\n group-configs:\n - group: \'default\'\n paths-to-match: \'/**\'\n packages-to-scan: \n - com.storm.device.controller','539ba97226dc0ba4e96a4d783231f28a','2025-08-26 16:24:27','2025-08-26 08:24:27','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,28,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n# package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html\n enabled: false # 禁用默认的Swagger UI,使用Knife4j的UI\n config-url: /v3/api-docs/default','d1c4c0545ca761da553b8b82f8c8e48e','2025-08-26 16:26:54','2025-08-26 08:26:55','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,29,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n# package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html','6eff4dbd509a0f95269c30b8ef2901be','2025-08-26 16:31:43','2025-08-26 08:31:44','nacos','192.168.48.1','U','','','formal',NULL,NULL),(5,30,'storm-system-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: storm\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n connectTimeout: 30000\n socketTimeout: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: 123456\n # url: \n # driver-class-name: \n\n# springdoc配置\n#springdoc:\n# gatewayUrl: http://localhost:8080/${spring.application.name}\n# api-docs:\n # 是否开启接口文档\n# enabled: true\n# info:\n # 标题\n# title: \'系统模块接口文档\'\n # 描述\n# description: \'系统模块接口描述\'\n # 作者信息\n# contact:\n# name: storm\n# url: https://storm.vip\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.system.controller\n title: 具身风暴 - 系统模块接口文档\n description: 该服务包含系统配置有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0','73b3f9d3c2526078b12e00e1b12908b8','2025-08-26 16:36:24','2025-08-26 08:36:25','nacos','192.168.48.1','U','','','formal',NULL,NULL),(6,31,'storm-gen-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.gen.controller\n title: 具身风暴 - 代码生成接口文档\n description: 该服务包含代码生成有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n\n# 代码生成\ngen:\n # 作者\n author: storm\n # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool\n packageName: com.storm.system\n # 自动去除表前缀,默认是false\n autoRemovePre: false\n # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)\n tablePrefix: sys_\n # 是否允许生成文件覆盖到本地(自定义路径),默认不允许\n allowOverwrite: false','ba27c925836b36128ec691767631af19','2025-08-26 16:36:43','2025-08-26 08:36:44','nacos','192.168.48.1','U','','','formal',NULL,NULL),(7,32,'storm-job-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.job.controller\n title: 具身风暴 - 定时任务接口文档\n description: 该服务包含定时任务有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0','f4b90997fcdf93706a38d6c040f3617b','2025-08-26 16:36:57','2025-08-26 08:36:58','nacos','192.168.48.1','U','','','formal',NULL,NULL),(2,33,'storm-gateway-dev.yml','DEFAULT_GROUP','','spring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n cloud:\n gateway:\n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: storm-auth\n uri: lb://storm-auth\n predicates:\n - Path=/auth/**\n filters:\n # 验证码处理\n - CacheRequestBody\n - ValidateCodeFilter\n - StripPrefix=1\n # 代码生成\n - id: storm-gen\n uri: lb://storm-gen\n predicates:\n - Path=/code/**\n filters:\n - StripPrefix=1\n # 定时任务\n - id: storm-job\n uri: lb://storm-job\n predicates:\n - Path=/schedule/**\n filters:\n - StripPrefix=1\n # 系统模块\n - id: storm-system\n uri: lb://storm-system\n predicates:\n - Path=/system/**\n filters:\n - StripPrefix=1\n # 文件服务\n - id: storm-file\n uri: lb://storm-file\n predicates:\n - Path=/file/**\n filters:\n - StripPrefix=1\n #设备管理\n - id: storm-device\n uri: lb://storm-device\n predicates:\n - Path=/device/**\n filters:\n - StripPrefix=1\n\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n - /system/notice\n\n # 不校验白名单\n ignore:\n whites:\n - /auth/logout\n - /auth/login\n - /auth/register\n - /*/v2/api-docs\n - /*/v3/api-docs\n - /csrf\n\n# springdoc配置\nspringdoc:\n webjars:\n # 访问前缀\n prefix:\n# knife4j配置\nknife4j:\n enable: true # 启用Knife4j\n gateway:\n enabled: true # 启用网关聚合功能\n strategy: discover # 使用服务发现模式进行聚合\n discover:\n enabled: true # 启用服务发现\n version: openapi3 # 指定OpenAPI版本,与你使用的规范一致','79ad193ece921ff4f1420f74506fbab0','2025-08-26 16:51:11','2025-08-26 08:51:12','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,34,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html','5918899d59d3c118910901e68de4235c','2025-08-27 16:23:07','2025-08-27 08:23:07','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,35,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html\n\n# MyBatisPlus配置\nmybatis-plus:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.**.domain\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath*:mapper/**/*Mapper.xml\n # 全局配置\n global-config:\n db-config:\n id-type: auto #id生成策略为自增\n configuration:\n map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名','e22b2502c706863ecb11702acfb24640','2025-08-29 10:16:20','2025-08-29 02:16:21','nacos','192.168.48.1','U','','','formal',NULL,NULL),(2,36,'storm-gateway-dev.yml','DEFAULT_GROUP','','spring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n cloud:\n gateway:\n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: storm-auth\n uri: lb://storm-auth\n predicates:\n - Path=/auth/**\n filters:\n # 验证码处理\n - CacheRequestBody\n - ValidateCodeFilter\n - StripPrefix=1\n # 代码生成\n - id: storm-gen\n uri: lb://storm-gen\n predicates:\n - Path=/code/**\n filters:\n - StripPrefix=1\n # 定时任务\n - id: storm-job\n uri: lb://storm-job\n predicates:\n - Path=/schedule/**\n filters:\n - StripPrefix=1\n # 系统模块\n - id: storm-system\n uri: lb://storm-system\n predicates:\n - Path=/system/**\n filters:\n - StripPrefix=1\n # 文件服务\n - id: storm-file\n uri: lb://storm-file\n predicates:\n - Path=/file/**\n filters:\n - StripPrefix=1\n #设备管理\n - id: storm-device\n uri: lb://storm-device\n predicates:\n - Path=/device/**\n filters:\n - StripPrefix=1\n\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n - /system/notice\n\n # 不校验白名单\n ignore:\n whites:\n - /auth/logout\n - /auth/login\n - /auth/register\n - /*/v2/api-docs\n - /*/v3/api-docs\n - /csrf\n\n# springdoc配置\nspringdoc:\n webjars:\n # 访问前缀\n prefix:\n# knife4j配置\nknife4j:\n enable: true # 启用Knife4j\n gateway:\n enabled: true # 启用网关聚合功能\n strategy: discover # 使用服务发现模式进行聚合\n discover:\n enabled: true # 启用服务发现\n version: openapi3 # 指定OpenAPI版本,与你使用的规范一致\n all-services: true # 聚合所有服务\n group-name: default # 默认分组名称','e55438f013e841a3d20bf57cd8f6350d','2025-09-03 20:29:11','2025-09-03 12:29:11','nacos','192.168.48.1','U','','','formal',NULL,NULL),(3,37,'storm-auth-dev.yml','DEFAULT_GROUP','','spring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n','0a6b4c5080d77aa4e9e34fbed3e6d51c','2025-09-03 20:29:38','2025-09-03 12:29:38','nacos','192.168.48.1','U','','','formal',NULL,NULL),(5,38,'storm-system-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: storm\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n connectTimeout: 30000\n socketTimeout: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n # 从库数据源\n # slave:\n # username: \n # password: 123456\n # url: \n # driver-class-name: \n\n# springdoc配置\n#springdoc:\n# gatewayUrl: http://localhost:8080/${spring.application.name}\n# api-docs:\n # 是否开启接口文档\n# enabled: true\n# info:\n # 标题\n# title: \'系统模块接口文档\'\n # 描述\n# description: \'系统模块接口描述\'\n # 作者信息\n# contact:\n# name: storm\n# url: https://storm.vip\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.system.controller\n title: 具身风暴 - 系统模块接口文档\n description: 该服务包含系统配置有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html','efda4f9733019744d8d7674e994b7fbc','2025-09-03 20:30:53','2025-09-03 12:30:53','nacos','192.168.48.1','U','','','formal',NULL,NULL),(6,39,'storm-gen-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.gen.controller\n title: 具身风暴 - 代码生成接口文档\n description: 该服务包含代码生成有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html\n\n# 代码生成\ngen:\n # 作者\n author: storm\n # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool\n packageName: com.storm.system\n # 自动去除表前缀,默认是false\n autoRemovePre: false\n # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)\n tablePrefix: sys_\n # 是否允许生成文件覆盖到本地(自定义路径),默认不允许\n allowOverwrite: false','bd39adf6ba0c4c85316cd36a20ccb1e4','2025-09-03 20:31:37','2025-09-03 12:31:38','nacos','192.168.48.1','U','','','formal',NULL,NULL),(7,40,'storm-job-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.job.controller\n title: 具身风暴 - 定时任务接口文档\n description: 该服务包含定时任务有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html','5ce2556fb86ab36e31becd3d3b78b186','2025-09-03 20:32:17','2025-09-03 12:32:18','nacos','192.168.48.1','U','','','formal',NULL,NULL),(10,41,'storm-device-dev.yml','DEFAULT_GROUP','','# spring配置\nspring:\n redis:\n host: 192.168.48.128\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://192.168.48.128:3306/storm-device?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: 123456\n\n# knife4j配置\nstorm:\n swagger:\n enable: true\n enableResponseWrap: true\n gatewayUrl: http://localhost:8080/${spring.application.name}\n package-path: com.storm.device.controller\n title: 具身风暴 - 设备管理接口文档\n description: 该服务包含设备有关的功能\n contact-name: storm\n contact-url: https://storm.vip\n contact-email: zx1697935564@163.com\n version: v1.0\n\n# 添加SpringDoc基本配置\nspringdoc:\n api-docs:\n path: /v3/api-docs\n enabled: true\n swagger-ui:\n path: /swagger-ui.html\n','74442fb4d97c82eff077a4aa63fbe27f','2025-09-03 20:32:59','2025-09-03 12:33:00','nacos','192.168.48.1','U','','','formal',NULL,NULL); +/*!40000 ALTER TABLE `his_config_info` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `permissions` +-- + +DROP TABLE IF EXISTS `permissions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `permissions` ( + `role` varchar(50) NOT NULL COMMENT 'role', + `resource` varchar(128) NOT NULL COMMENT 'resource', + `action` varchar(8) NOT NULL COMMENT 'action', + UNIQUE KEY `uk_role_permission` (`role`,`resource`,`action`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `permissions` +-- + +LOCK TABLES `permissions` WRITE; +/*!40000 ALTER TABLE `permissions` DISABLE KEYS */; +/*!40000 ALTER TABLE `permissions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `roles` +-- + +DROP TABLE IF EXISTS `roles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `roles` ( + `username` varchar(50) NOT NULL COMMENT 'username', + `role` varchar(50) NOT NULL COMMENT 'role', + UNIQUE KEY `idx_user_role` (`username`,`role`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `roles` +-- + +LOCK TABLES `roles` WRITE; +/*!40000 ALTER TABLE `roles` DISABLE KEYS */; +INSERT INTO `roles` VALUES ('nacos','ROLE_ADMIN'); +/*!40000 ALTER TABLE `roles` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `tenant_capacity` +-- + +DROP TABLE IF EXISTS `tenant_capacity`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `tenant_capacity` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tenant_id` varchar(128) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'Tenant ID', + `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', + `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', + `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', + `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数', + `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', + `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `tenant_capacity` +-- + +LOCK TABLES `tenant_capacity` WRITE; +/*!40000 ALTER TABLE `tenant_capacity` DISABLE KEYS */; +/*!40000 ALTER TABLE `tenant_capacity` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `tenant_info` +-- + +DROP TABLE IF EXISTS `tenant_info`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `tenant_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `kp` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'kp', + `tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id', + `tenant_name` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_name', + `tenant_desc` varchar(256) COLLATE utf8_bin DEFAULT NULL COMMENT 'tenant_desc', + `create_source` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'create_source', + `gmt_create` bigint(20) NOT NULL COMMENT '创建时间', + `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), + KEY `idx_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `tenant_info` +-- + +LOCK TABLES `tenant_info` WRITE; +/*!40000 ALTER TABLE `tenant_info` DISABLE KEYS */; +/*!40000 ALTER TABLE `tenant_info` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `users` +-- + +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `users` ( + `username` varchar(50) NOT NULL COMMENT 'username', + `password` varchar(500) NOT NULL COMMENT 'password', + `enabled` tinyint(1) NOT NULL COMMENT 'enabled', + PRIMARY KEY (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `users` +-- + +LOCK TABLES `users` WRITE; +/*!40000 ALTER TABLE `users` DISABLE KEYS */; +INSERT INTO `users` VALUES ('nacos','$2a$10$cMvFSQiCPJN8k/3BIeEzducWdh9CD8y4PTKGYRfDNajhvE.94Du5O',1); +/*!40000 ALTER TABLE `users` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping routines for database 'storm-config' +-- +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-04 10:19:06 diff --git a/docker/mysql/db/dump-storm-device-202509032130.sql b/docker/mysql/db/dump-storm-device-202509032130.sql new file mode 100644 index 0000000..1f2eded --- /dev/null +++ b/docker/mysql/db/dump-storm-device-202509032130.sql @@ -0,0 +1,151 @@ +-- MySQL dump 10.13 Distrib 8.0.42, for Win64 (x86_64) +-- +-- Host: 192.168.48.128 Database: storm-device +-- ------------------------------------------------------ +-- Server version 5.7.44 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `device` +-- + +USE `storm-device`; + +DROP TABLE IF EXISTS `device`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `device` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `device_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备唯一标识', + `device_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备名称', + `remark` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', + `product_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品ID', + `product_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品名称', + `device_type` int(15) DEFAULT NULL COMMENT '设备类型(1:RS-LL-X1,2:RS-LL-X2)', + `location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '安装位置', + `shop_id` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '店铺ID', + `longitude` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '经度', + `latitude` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '纬度', + `software_version` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '软件版本', + `vtxdb_version` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '参数服务器版本', + `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备描述', + `is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除(0未删除,1已删除)', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建人ID', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新人ID', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `status` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'OFFLINE' COMMENT '设备状态(在线ONLINE/下线OFFLINE)', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_device_id` (`device_id`), + UNIQUE KEY `uk_device_name` (`device_name`), + KEY `idx_product_id` (`product_id`), + KEY `idx_device_type` (`device_type`), + KEY `idx_shop_id` (`shop_id`), + KEY `idx_location` (`location`(50)), + KEY `idx_is_deleted` (`is_deleted`) +) ENGINE=InnoDB AUTO_INCREMENT=429 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备基本信息表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `device` +-- + +LOCK TABLES `device` WRITE; +/*!40000 ALTER TABLE `device` DISABLE KEYS */; +INSERT INTO `device` VALUES (201,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'河南省南阳市宛城区东关街道公共厕所附近14米',NULL,'112.555042','33.003268','2.0.0-dev-rc8-cn','lite-1.0-rc18',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:04','OFFLINE'),(202,'673602f0d14760402fbd033b_jsfb-B46F2C','jsfb-B46F2C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(203,'673602f0d14760402fbd033b_jsfb-151528','jsfb-151528',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(204,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省珠海市香洲区横琴镇横琴口岸',NULL,'113.558044','22.144445','2.1.0-dev-rc4-cn','lite-1.1-rc3',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:04','OFFLINE'),(205,'673602f0d14760402fbd033b_jsfb-9CC71E','jsfb-9CC71E',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(206,'673602f0d14760402fbd033b_jsfb-345EAC','jsfb-345EAC',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:34:04','ONLINE'),(207,'673602f0d14760402fbd033b_jsfb-F295AF','jsfb-F295AF',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(208,'673602f0d14760402fbd033b_jsfb-D6FA0B','jsfb-D6FA0B',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(209,'673602f0d14760402fbd033b_jsfb-EEACB2','jsfb-EEACB2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(210,'673602f0d14760402fbd033b_jsfb-B60904','jsfb-B60904',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(211,'673602f0d14760402fbd033b_jsfb-EDEA70','jsfb-EDEA70',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(212,'673602f0d14760402fbd033b_jsfb-test','jsfb-test',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 14:09:18','ONLINE'),(213,'673602f0d14760402fbd033b_jsfb-4D35B0','jsfb-4D35B0',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(214,'673602f0d14760402fbd033b_jsfb-A0B2F9','jsfb-A0B2F9',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(215,'673602f0d14760402fbd033b_jsfb-AA3B31','jsfb-AA3B31',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(216,'673602f0d14760402fbd033b_jsfb-82EBA4','jsfb-82EBA4',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(217,'673602f0d14760402fbd033b_jsfb-D1960D','jsfb-D1960D',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(218,'673602f0d14760402fbd033b_jsfb-0F1B1C','jsfb-0F1B1C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(219,'673602f0d14760402fbd033b_jsfb-6502FF','jsfb-6502FF',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(220,'673602f0d14760402fbd033b_jsfb-8A3D08','jsfb-8A3D08',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(221,'673602f0d14760402fbd033b_jsfb-746B1E','jsfb-746B1E',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(222,'673602f0d14760402fbd033b_jsfb-33F73F','jsfb-33F73F',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(223,'6752ca58d14760402fc2fdc4_otatest','otatest',NULL,'6752ca58d14760402fc2fdc4','wmz-product',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(224,'6752ca58d14760402fc2fdc4_model','model',NULL,'6752ca58d14760402fc2fdc4','wmz-product',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(225,'673602f0d14760402fbd033b_jsfb-test2','jsfb-test2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(226,'6752ca58d14760402fc2fdc4_Test_1','test_1',NULL,'6752ca58d14760402fc2fdc4','wmz-product',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 14:46:48','OFFLINE'),(228,'673602f0d14760402fbd033b_wmz_01','wmz_test',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-08-28 15:19:26','OFFLINE'),(229,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'',NULL,'112.541115','28.280528','ioT','1.1.9-beta-ansun',NULL,0,NULL,NULL,NULL,'2025-09-03 16:49:50','OFFLINE'),(230,'673602f0d14760402fbd033b_jsfb-023486','jsfb-023486',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(231,'673602f0d14760402fbd033b_jsfb-6FF12E','jsfb-6FF12E',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(232,'673602f0d14760402fbd033b_jsfb-62B336','jsfb-62B336',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(233,'673602f0d14760402fbd033b_jsfb-F706AD','jsfb-F706AD',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(234,'673602f0d14760402fbd033b_jsfb-50441C','jsfb-50441C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(235,'673602f0d14760402fbd033b_jsfb-B25A00','jsfb-B25A00',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(236,'673602f0d14760402fbd033b_jsfb-DCF894','jsfb-DCF894',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(237,'673602f0d14760402fbd033b_jsfb-6F0F92','jsfb-6F0F92',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(238,'673602f0d14760402fbd033b_jsfb-5A1B1E','jsfb-5A1B1E',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(239,'673602f0d14760402fbd033b_jsfb-1DB8E2','jsfb-1DB8E2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(240,'673602f0d14760402fbd033b_jsfb-FD7C6B','jsfb-FD7C6B',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(241,'673602f0d14760402fbd033b_jsfb-3554A0','jsfb-3554A0',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(242,'673602f0d14760402fbd033b_jsfb-BD51A6','jsfb-BD51A6',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(243,'673602f0d14760402fbd033b_jsfb-98BF09','jsfb-98BF09',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(244,'673602f0d14760402fbd033b_jsfb-B6A9C3','jsfb-B6A9C3',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(245,'673602f0d14760402fbd033b_jsfb-EBF1D8','jsfb-EBF1D8',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(246,'673602f0d14760402fbd033b_jsfb-65024A','jsfb-65024A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(247,'673602f0d14760402fbd033b_jsfb-7FC7DD','jsfb-7FC7DD',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(248,'673602f0d14760402fbd033b_jsfb-7E9B30','jsfb-7E9B30',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(249,'673602f0d14760402fbd033b_jsfb-56A928','jsfb-56A928',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(250,'673602f0d14760402fbd033b_jsfb-F8843B','jsfb-F8843B',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(251,'673602f0d14760402fbd033b_jsfb-8AA2BD','jsfb-8AA2BD',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(252,'673602f0d14760402fbd033b_jsfb-C17E68','jsfb-C17E68',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(253,'673602f0d14760402fbd033b_jsfb-7BE292','jsfb-7BE292',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(254,'673602f0d14760402fbd033b_jsfb-6A1F29','jsfb-6A1F29',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(255,'673602f0d14760402fbd033b_jsfb-B75756','jsfb-B75756',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(256,'673602f0d14760402fbd033b_jsfb-69B0D2','jsfb-69B0D2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(257,'673602f0d14760402fbd033b_jsfb-D120B0','jsfb-D120B0',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(258,'673602f0d14760402fbd033b_jsfb-CA0D7A','jsfb-CA0D7A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(259,'673602f0d14760402fbd033b_jsfb-89122A','jsfb-89122A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(260,'673602f0d14760402fbd033b_jsfb-31464E','jsfb-31464E',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(261,'673602f0d14760402fbd033b_jsfb-2A1577','jsfb-2A1577',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(262,'673602f0d14760402fbd033b_jsfb-1EC625','jsfb-1EC625',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(263,'673602f0d14760402fbd033b_jsfb-36350C','jsfb-36350C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(264,'673602f0d14760402fbd033b_jsfb-F29258','jsfb-F29258',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(265,'673602f0d14760402fbd033b_jsfb-8942CE','jsfb-8942CE',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(266,'673602f0d14760402fbd033b_jsfb-B1398A','jsfb-B1398A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(267,'673602f0d14760402fbd033b_jsfb-465F0C','jsfb-465F0C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(268,'673602f0d14760402fbd033b_jsfb-E169C2','jsfb-E169C2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(269,'673602f0d14760402fbd033b_jsfb-E82E16','jsfb-E82E16',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(270,'673602f0d14760402fbd033b_jsfb-6FA0C5','jsfb-6FA0C5',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(271,'673602f0d14760402fbd033b_jsfb-FC04F2','jsfb-FC04F2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(272,'673602f0d14760402fbd033b_jsfb-C5D5F4','jsfb-C5D5F4',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(273,'673602f0d14760402fbd033b_jsfb-B96E18','jsfb-B96E18',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(274,'673602f0d14760402fbd033b_jsfb-37285C','jsfb-37285C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(275,'673602f0d14760402fbd033b_jsfb-00A874','jsfb-00A874',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(276,'673602f0d14760402fbd033b_jsfb-7D2A0B','jsfb-7D2A0B',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(277,'673602f0d14760402fbd033b_jsfb-69681C','jsfb-69681C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(278,'673602f0d14760402fbd033b_jsfb-6D10F3','jsfb-6D10F3',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(279,'673602f0d14760402fbd033b_jsfb-C4320C','jsfb-C4320C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(280,'673602f0d14760402fbd033b_jsfb-CF2180','jsfb-CF2180',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(281,'673602f0d14760402fbd033b_jsfb-3375F3','jsfb-3375F3',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(282,'673602f0d14760402fbd033b_jsfb-B45F7A','jsfb-B45F7A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(283,'673602f0d14760402fbd033b_jsfb-B6BECF','jsfb-B6BECF',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(284,'673602f0d14760402fbd033b_jsfb-D2A5AA','jsfb-D2A5AA',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(285,'673602f0d14760402fbd033b_jsfb-121194','jsfb-121194',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(286,'673602f0d14760402fbd033b_jsfb-7AE86C','jsfb-7AE86C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(287,'673602f0d14760402fbd033b_jsfb-D4BCAE','jsfb-D4BCAE',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(288,'673602f0d14760402fbd033b_jsfb-7CFAA9','jsfb-7CFAA9',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(289,'673602f0d14760402fbd033b_jsfb-223A77','jsfb-223A77',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(290,'673602f0d14760402fbd033b_jsfb-327563','jsfb-327563',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(291,'673602f0d14760402fbd033b_jsfb-38F531','jsfb-38F531',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(292,'673602f0d14760402fbd033b_jsfb-AAD08A','jsfb-AAD08A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(293,'673602f0d14760402fbd033b_jsfb-52987B','jsfb-52987B',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(294,'673602f0d14760402fbd033b_jsfb-66CEDE','jsfb-66CEDE',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(295,'673602f0d14760402fbd033b_jsfb-6C5C92','jsfb-6C5C92',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(296,'673602f0d14760402fbd033b_jsfb-F7286E','jsfb-F7286E',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 16:18:09','ONLINE'),(297,'673602f0d14760402fbd033b_jsfb-711043','jsfb-711043',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(298,'673602f0d14760402fbd033b_jsfb-AED627','jsfb-AED627',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(299,'673602f0d14760402fbd033b_jsfb-D02B2A','jsfb-D02B2A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(300,'673602f0d14760402fbd033b_jsfb-9FE921','jsfb-9FE921',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(301,'673602f0d14760402fbd033b_jsfb-FEB841','jsfb-FEB841',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(302,'673602f0d14760402fbd033b_jsfb-B319CD','jsfb-B319CD',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(303,'673602f0d14760402fbd033b_jsfb-3DB5BD','jsfb-3DB5BD',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(304,'673602f0d14760402fbd033b_jsfb-66E903','jsfb-66E903',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:58:47','ONLINE'),(305,'673602f0d14760402fbd033b_jsfb-3AE8E2','jsfb-3AE8E2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(306,'673602f0d14760402fbd033b_jsfb-C807AE','jsfb-C807AE',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 16:55:05','ONLINE'),(307,'673602f0d14760402fbd033b_jsfb-301B84','jsfb-301B84',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(308,'673602f0d14760402fbd033b_jsfb-7EA0F7','jsfb-7EA0F7',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(309,'673602f0d14760402fbd033b_jsfb-6D3F04','jsfb-6D3F04',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','ONLINE'),(310,'673602f0d14760402fbd033b_jsfb-7CD788','jsfb-7CD788',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','ONLINE'),(311,'673602f0d14760402fbd033b_jsfb-3411D0','jsfb-3411D0',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(312,'673602f0d14760402fbd033b_jsfb-1573FE','jsfb-1573FE',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 16:27:37','ONLINE'),(313,'673602f0d14760402fbd033b_jsfb-640DDE','jsfb-640DDE',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:34:04','OFFLINE'),(314,'673602f0d14760402fbd033b_jsfb-4C4E81','jsfb-4C4E81',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(315,'673602f0d14760402fbd033b_jsfb-C593E4','jsfb-C593E4',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:34:04','ONLINE'),(316,'673602f0d14760402fbd033b_jsfb-6F3778','jsfb-6F3778',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 14:09:18','ONLINE'),(317,'673602f0d14760402fbd033b_jsfb-0FD846','jsfb-0FD846',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(318,'673602f0d14760402fbd033b_jsfb-35E8F3','jsfb-35E8F3',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(319,'673602f0d14760402fbd033b_jsfb-FE0E1F','jsfb-FE0E1F',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(320,'673602f0d14760402fbd033b_jsfb-BEA6F3','jsfb-BEA6F3',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(321,'673602f0d14760402fbd033b_jsfb-F6D166','jsfb-F6D166',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','ONLINE'),(322,'673602f0d14760402fbd033b_jsfb-95A974','jsfb-95A974',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(323,'673602f0d14760402fbd033b_jsfb-E67F68','jsfb-E67F68',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 16:18:09','ONLINE'),(324,'673602f0d14760402fbd033b_jsfb-DB193B','jsfb-DB193B',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:34:04','ONLINE'),(325,'673602f0d14760402fbd033b_jsfb-FD1F8E','jsfb-FD1F8E',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(326,'673602f0d14760402fbd033b_jsfb-B5CB19','jsfb-B5CB19',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 11:43:27','ONLINE'),(327,'673602f0d14760402fbd033b_jsfb-2E67F0','jsfb-2E67F0',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(328,'673602f0d14760402fbd033b_jsfb-39D61A','jsfb-39D61A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 16:18:09','ONLINE'),(329,'673602f0d14760402fbd033b_jsfb-282C46','jsfb-282C46',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 16:35:43','ONLINE'),(330,'673602f0d14760402fbd033b_jsfb-F99E5C','jsfb-F99E5C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(331,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省深圳市宝安区西乡街道涉外高尔夫俱乐部(宝安碧海湾)',NULL,'113.856885','22.591792','2.1.0-dev-rc4-cn','lite-1.1-rc2',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:04','ONLINE'),(332,'673602f0d14760402fbd033b_jsfb-wmz06','jsfb-wmz06',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(333,'673602f0d14760402fbd033b_jsfb-wmz05','jsfb-wmz05',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省深圳市龙华区民治街道深圳市深水龙华水务有限公司',NULL,'114.026404','22.62023','1.2.9-alpha-cn','default',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:05','ONLINE'),(334,'673602f0d14760402fbd033b_jsfb-4C1004','jsfb-4C1004',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(335,'673602f0d14760402fbd033b_jsfb-F41A8A','jsfb-F41A8A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(336,'673602f0d14760402fbd033b_jsfb-CE45B3','jsfb-CE45B3',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(337,'673602f0d14760402fbd033b_jsfb-83EE73','jsfb-83EE73',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(338,'673602f0d14760402fbd033b_jsfb-969F7F','jsfb-969F7F',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(339,'673602f0d14760402fbd033b_jsfb-1218D4','jsfb-1218D4',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(340,'673602f0d14760402fbd033b_jsfb-D1D94F','jsfb-D1D94F',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(341,'673602f0d14760402fbd033b_jsfb-188348','jsfb-188348',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(342,'673602f0d14760402fbd033b_jsfb-B4EFB7','jsfb-B4EFB7',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 12:48:35','ONLINE'),(343,'673602f0d14760402fbd033b_jsfb-E7326A','jsfb-E7326A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(344,'673602f0d14760402fbd033b_jsfb-9729D1','jsfb-9729D1',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 14:09:18','ONLINE'),(345,'673602f0d14760402fbd033b_jsfb-5EC2B8','jsfb-5EC2B8',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'江苏省南京市栖霞区栖霞街道妙品渔具南169米',NULL,'118.951637','32.150456','2.0.0-alpha-rc12-cn','lite-1.0-rc18',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:05','OFFLINE'),(346,'673602f0d14760402fbd033b_jsfb-CC4938','jsfb-CC4938',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(347,'673602f0d14760402fbd033b_jsfb-DB1938','jsfb-DB1938',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(348,'673602f0d14760402fbd033b_jsfb-2BD71F','jsfb-2BD71F',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(349,'673602f0d14760402fbd033b_jsfb-AA0207','jsfb-AA0207',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 14:09:18','ONLINE'),(350,'673602f0d14760402fbd033b_jsfb-EAD6D9','jsfb-EAD6D9',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(351,'673602f0d14760402fbd033b_jsfb-A1AFEF','jsfb-A1AFEF',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 14:09:18','ONLINE'),(352,'673602f0d14760402fbd033b_jsfb-0D1FF1','jsfb-0D1FF1',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(353,'673602f0d14760402fbd033b_jsfb-CBB1CE','jsfb-CBB1CE',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 14:11:05','ONLINE'),(354,'673602f0d14760402fbd033b_jsfb-29D1DB','jsfb-29D1DB',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(355,'673602f0d14760402fbd033b_jsfb-EE4C3F','jsfb-EE4C3F',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:34:04','ONLINE'),(356,'673602f0d14760402fbd033b_jsfb-D11A27','jsfb-D11A27',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(357,'673602f0d14760402fbd033b_jsfb-729C78','jsfb-729C78',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(358,'673602f0d14760402fbd033b_jsfb-08C571','jsfb-08C571',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(359,'673602f0d14760402fbd033b_jsfb-960460','jsfb-960460',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:34:04','ONLINE'),(360,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(361,'673602f0d14760402fbd033b_jsfb-AA0107','jsfb-AA0107',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','ONLINE'),(362,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,'114.026594','22.621167','2.1.0-dev-rc2-cn','lite-1.0-rc18',NULL,0,NULL,NULL,NULL,'2025-09-03 14:35:32','OFFLINE'),(363,'673602f0d14760402fbd033b_jsfb-AE30E0','jsfb-AE30E0',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 16:18:09','ONLINE'),(364,'673602f0d14760402fbd033b_jsfb-0322F9','jsfb-0322F9',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:55:11','ONLINE'),(365,'673602f0d14760402fbd033b_jsfb-DE34AD','jsfb-DE34AD',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(366,'673602f0d14760402fbd033b_jsfb-589DF2','jsfb-589DF2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 14:09:18','ONLINE'),(367,'673602f0d14760402fbd033b_jsfb-2B6870','jsfb-2B6870',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:38:37','ONLINE'),(368,'673602f0d14760402fbd033b_jsfb-82FB71','jsfb-82FB71',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','ONLINE'),(369,'673602f0d14760402fbd033b_jsfb-A7C874','jsfb-A7C874',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','ONLINE'),(370,'673602f0d14760402fbd033b_jsfb-17825D','jsfb-17825D',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(371,'673602f0d14760402fbd033b_jsfb-415303','jsfb-415303',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(372,'673602f0d14760402fbd033b_jsfb-48E2A5','jsfb-48E2A5',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省深圳市宝安区西乡街道涉外高尔夫俱乐部(宝安碧海湾)',NULL,'113.856885','22.591792','2.1.0-dev-rc4-cn','lite-1.0-rc18',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:05','ONLINE'),(373,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省深圳市宝安区西乡街道涉外高尔夫俱乐部(宝安碧海湾)',NULL,'113.856836','22.591811','2.1.0-dev-rc4-cn','lite-1.0-rc18',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:05','ONLINE'),(374,'673602f0d14760402fbd033b_jsfb-FF0F7A','jsfb-FF0F7A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(375,'673602f0d14760402fbd033b_jsfb-EED4E5','jsfb-EED4E5',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 16:18:09','ONLINE'),(376,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:42:13','ONLINE'),(377,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:34:04','ONLINE'),(378,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','ONLINE'),(379,'673602f0d14760402fbd033b_jsfb-A80DF2','jsfb-A80DF2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 11:52:28','ONLINE'),(380,'673602f0d14760402fbd033b_jsfb-A5BC3B','jsfb-A5BC3B',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(381,'673602f0d14760402fbd033b_jsfb-EC7376','jsfb-EC7376',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','ONLINE'),(382,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:34:04','ONLINE'),(383,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 14:09:18','ONLINE'),(384,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 11:50:42','ONLINE'),(385,'673602f0d14760402fbd033b_jsfb-8162E2','jsfb-8162E2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(386,'673602f0d14760402fbd033b_jsfb-5C5AAC','jsfb-5C5AAC',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(387,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省深圳市龙华区民治街道深圳市龙华区生态文明展览馆西北295米',NULL,'114.026271','22.6206','2.0.0-dev-ion-cn','lite-1.1-rc3',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:05','OFFLINE'),(388,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省深圳市宝安区西乡街道涉外高尔夫俱乐部(宝安碧海湾)',NULL,'113.856836','22.591811','2.1.0-dev-rc3-cn','lite-1.0-rc18',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:04','ONLINE'),(389,'673602f0d14760402fbd033b_jsfb-8B3FFC','jsfb-8B3FFC',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(390,'673602f0d14760402fbd033b_jsfb-ED113F','jsfb-ED113F',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(391,'673602f0d14760402fbd033b_jsfb-F90990','jsfb-F90990',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 14:09:18','ONLINE'),(392,'673602f0d14760402fbd033b_jsfb-808955','jsfb-808955',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(393,'673602f0d14760402fbd033b_jsfb-C1128D','jsfb-C1128D',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(394,'673602f0d14760402fbd033b_jsfb-DD0159','jsfb-DD0159',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(395,'673602f0d14760402fbd033b_jsfb-lkr','jsfb-lkr',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省深圳市龙华区民治街道深圳市深水龙华水务有限公司',NULL,'114.026411','22.620293','1.3.1-alpha-rc2-cn','lite-1.0-rc18',NULL,0,NULL,NULL,NULL,'2025-09-03 14:15:53','ONLINE'),(396,'673602f0d14760402fbd033b_jsfb-6A4ACA','jsfb-6A4ACA',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(397,'673602f0d14760402fbd033b_jsfb-044962','jsfb-044962',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(398,'673602f0d14760402fbd033b_jsfb-372D8D','jsfb-372D8D',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(399,'673602f0d14760402fbd033b_jsfb-390B05','jsfb-390B05',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(400,'673602f0d14760402fbd033b_jsfb-A053C6','jsfb-A053C6',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(401,'673602f0d14760402fbd033b_jsfb-9E42A6','jsfb-9E42A6',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(402,'673602f0d14760402fbd033b_jsfb-9BF6F9','jsfb-9BF6F9',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(403,'673602f0d14760402fbd033b_jsfb-7F9FC2','jsfb-7F9FC2',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(404,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省深圳市宝安区西乡街道涉外高尔夫俱乐部(宝安碧海湾)',NULL,'113.856836','22.591811','2.1.0-dev-rc4-cn','lite-1.0-rc18',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:04','OFFLINE'),(405,'673602f0d14760402fbd033b_jsfb-3F6F58','jsfb-3F6F58',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(406,'673602f0d14760402fbd033b_jsfb-DD34FB','jsfb-DD34FB',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(407,'673602f0d14760402fbd033b_jsfb-FD3EC7','jsfb-FD3EC7',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(408,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省深圳市宝安区西乡街道涉外高尔夫俱乐部(宝安碧海湾)',NULL,'113.856836','22.591811','2.0.0-alpha-rc6-cn','lite-1.0-rc16.4',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:05','OFFLINE'),(409,'673602f0d14760402fbd033b_jsfb-0872E1','jsfb-0872E1',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(410,'673602f0d14760402fbd033b_jsfb-615B9A','jsfb-615B9A',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(411,'673602f0d14760402fbd033b_jsfb-E446E9','jsfb-E446E9',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(412,'673602f0d14760402fbd033b_jsfb-019799','jsfb-019799',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(413,'673602f0d14760402fbd033b_jsfb-BD0556','jsfb-BD0556',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(414,'673602f0d14760402fbd033b_jsfb-6E04FA','jsfb-6E04FA',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,'广东省深圳市龙华区民治街道深圳市龙华区生态文明展览馆西北288米',NULL,'114.026357','22.620568','2.0.0-alpha-rc10-cn','default',NULL,0,NULL,NULL,NULL,'2025-09-03 14:31:04','ONLINE'),(415,'673602f0d14760402fbd033b_jsfb-6D0FD5','jsfb-6D0FD5',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(416,'673602f0d14760402fbd033b_jsfb-2211BF','jsfb-2211BF',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 11:15:56','ONLINE'),(417,'673602f0d14760402fbd033b_jsfb-D31D7B','jsfb-D31D7B',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(418,'673602f0d14760402fbd033b_jsfb-1695E8','jsfb-1695E8',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(419,'673602f0d14760402fbd033b_jsfb-231936','jsfb-231936',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-03 10:02:32','ONLINE'),(420,'673602f0d14760402fbd033b_jsfb-8D2128','jsfb-8D2128',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','ONLINE'),(421,'673602f0d14760402fbd033b_jsfb-074FA9','jsfb-074FA9',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(422,'673602f0d14760402fbd033b_jsfb-84759C','jsfb-84759C',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(423,'673602f0d14760402fbd033b_jsfb-AD49A4','jsfb-AD49A4',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(424,'673602f0d14760402fbd033b_jsfb-4AAE47','jsfb-4AAE47',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,'135.505541','34.669074','2.1.0-dev-rc4-cn','lite-1.0-rc18',NULL,0,NULL,NULL,NULL,'2025-09-03 14:34:59','OFFLINE'),(425,'673602f0d14760402fbd033b_jsfb-04ACBC','jsfb-04ACBC',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(426,'673602f0d14760402fbd033b_jsfb-D0D651','jsfb-D0D651',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','OFFLINE'),(427,'673602f0d14760402fbd033b_jsfb-36FE03','jsfb-36FE03',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-01 10:09:47','INACTIVE'),(428,'673602f0d14760402fbd033b_jsfb-216A14','jsfb-216A14',NULL,'673602f0d14760402fbd033b','RS-LL-X1',1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,NULL,NULL,NULL,'2025-09-02 10:34:04','ONLINE'); +/*!40000 ALTER TABLE `device` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `device_status_log` +-- + +DROP TABLE IF EXISTS `device_status_log`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `device_status_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `device_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备ID', + `status` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备状态(ONLINE/OFFLINE)', + `event_time` datetime NOT NULL COMMENT '状态变更时间', + `last_online_time` datetime DEFAULT NULL COMMENT '最后在线时间(仅OFFLINE时有值)', + `duration` bigint(20) DEFAULT NULL COMMENT '本次在线持续时间(秒)', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + PRIMARY KEY (`id`), + KEY `idx_device_id` (`device_id`), + KEY `idx_status` (`status`), + KEY `idx_event_time` (`event_time`), + KEY `idx_device_status` (`device_id`,`status`) +) ENGINE=InnoDB AUTO_INCREMENT=80 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备状态日志表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `device_status_log` +-- + +LOCK TABLES `device_status_log` WRITE; +/*!40000 ALTER TABLE `device_status_log` DISABLE KEYS */; +INSERT INTO `device_status_log` VALUES (4,'673602f0d14760402fbd033b_jsfb-DA','ONLINE','2025-09-01 15:40:03','2025-09-01 15:40:02',NULL,NULL),(5,'673602f0d14760402fbd033b_jsfb-DA','OFFLINE','2025-09-01 15:48:05','2025-09-01 15:48:05',483,NULL),(6,'673602f0d14760402fbd033b_jsfb-DA','OFFLINE','2025-09-01 17:41:51','2025-09-01 17:41:51',221,NULL),(7,'673602f0d14760402fbd033b_jsfb-DA','OFFLINE','2025-09-01 19:38:31','2025-09-01 19:38:31',0,'2025-09-01 19:40:21'),(8,'673602f0d14760402fbd033b_jsfb-DA','OFFLINE','2025-09-01 19:41:13','2025-09-01 19:41:13',0,'2025-09-01 19:41:45'),(9,'673602f0d14760402fbd033b_jsfb-DA','OFFLINE','2025-09-01 19:45:33','2025-09-01 19:45:33',0,'2025-09-01 19:46:07'),(10,'673602f0d14760402fbd033b_jsfb-DA','OFFLINE','2025-09-01 19:48:05','2025-09-01 19:48:05',0,'2025-09-01 19:48:50'),(11,'673602f0d14760402fbd033b_jsfb-9729D1','OFFLINE','2025-09-01 19:54:57','2025-09-01 19:54:57',0,'2025-09-01 19:56:35'),(12,'673602f0d14760402fbd033b_jsfb-A1AFEF','OFFLINE','2025-09-01 20:07:12','2025-09-01 20:07:12',-1,'2025-09-01 20:07:13'),(13,'673602f0d14760402fbd033b_jsfb-0322F9','OFFLINE','2025-09-01 20:00:05','2025-09-01 20:00:05',0,'2025-09-01 20:10:38'),(14,'673602f0d14760402fbd033b_jsfb-E67F68','OFFLINE','2025-09-01 20:10:58','2025-09-01 20:10:58',0,'2025-09-01 20:19:50'),(15,'673602f0d14760402fbd033b_jsfb-E67F68','OFFLINE','2025-09-01 20:20:42','2025-09-01 20:20:42',0,'2025-09-01 20:26:51'),(16,'673602f0d14760402fbd033b_jsfb-E67F68','OFFLINE','2025-09-01 20:26:51','2025-09-01 20:26:51',0,'2025-09-01 20:26:55'),(17,'673602f0d14760402fbd033b_jsfb-DA','OFFLINE','2025-09-01 20:43:03','2025-09-01 20:43:03',29,'2025-09-01 20:43:28'),(18,'673602f0d14760402fbd033b_jsfb-39D61A','OFFLINE','2025-09-01 20:44:38','2025-09-01 20:44:38',29,'2025-09-01 20:44:39'),(19,'673602f0d14760402fbd033b_jsfb-D9CC3E','OFFLINE','2025-09-01 21:21:02','2025-09-01 21:21:02',-1,'2025-09-02 10:34:04'),(20,'673602f0d14760402fbd033b_jsfb-39D61A','OFFLINE','2025-09-01 20:59:02','2025-09-01 20:59:02',857,'2025-09-02 10:34:04'),(21,'673602f0d14760402fbd033b_jsfb-4C4E81','OFFLINE','2025-09-01 21:49:03','2025-09-01 21:49:03',1593,'2025-09-02 10:34:04'),(22,'673602f0d14760402fbd033b_jsfb-7EA0F7','OFFLINE','2025-09-01 22:21:10','2025-09-01 22:21:10',2403,'2025-09-02 10:34:04'),(23,'673602f0d14760402fbd033b_jsfb-7EA0F7','OFFLINE','2025-09-01 22:21:09','2025-09-01 22:21:09',2402,'2025-09-02 10:34:04'),(24,'673602f0d14760402fbd033b_jsfb-3411D0','OFFLINE','2025-09-01 22:25:19','2025-09-01 22:25:19',-134,'2025-09-02 10:34:04'),(25,'673602f0d14760402fbd033b_jsfb-0FD846','OFFLINE','2025-09-01 22:34:11','2025-09-01 22:34:11',797,'2025-09-02 10:34:04'),(26,'673602f0d14760402fbd033b_jsfb-3411D0','OFFLINE','2025-09-01 22:38:07','2025-09-01 22:38:07',553,'2025-09-02 10:34:04'),(27,'673602f0d14760402fbd033b_jsfb-6E04FA','OFFLINE','2025-09-01 22:49:56','2025-09-01 22:49:56',-8,'2025-09-02 10:34:04'),(28,'673602f0d14760402fbd033b_jsfb-9729D1','OFFLINE','2025-09-01 23:47:10','2025-09-01 23:47:10',1398,'2025-09-02 10:34:04'),(29,'673602f0d14760402fbd033b_jsfb-9729D1','OFFLINE','2025-09-01 23:47:11','2025-09-01 23:47:11',0,'2025-09-02 10:34:04'),(30,'673602f0d14760402fbd033b_jsfb-0FD846','OFFLINE','2025-09-01 23:49:35','2025-09-01 23:49:35',751,'2025-09-02 10:34:04'),(31,'673602f0d14760402fbd033b_jsfb-9729D1','OFFLINE','2025-09-01 23:54:40','2025-09-01 23:54:40',404,'2025-09-02 10:34:04'),(32,'673602f0d14760402fbd033b_jsfb-9729D1','OFFLINE','2025-09-01 23:54:41','2025-09-01 23:54:41',0,'2025-09-02 10:34:04'),(33,'673602f0d14760402fbd033b_jsfb-345EAC','OFFLINE','2025-09-02 00:18:29','2025-09-02 00:18:29',8113,'2025-09-02 10:34:04'),(34,'673602f0d14760402fbd033b_jsfb-301B84','OFFLINE','2025-09-01 22:04:43','2025-09-01 22:04:43',2488,'2025-09-02 10:34:04'),(35,'673602f0d14760402fbd033b_jsfb-3411D0','OFFLINE','2025-09-01 22:27:42','2025-09-01 22:27:42',9,'2025-09-02 10:34:04'),(36,'673602f0d14760402fbd033b_jsfb-3411D0','OFFLINE','2025-09-01 22:27:47','2025-09-01 22:27:47',14,'2025-09-02 10:34:04'),(37,'673602f0d14760402fbd033b_jsfb-EC37A5','OFFLINE','2025-09-02 08:25:44','2025-09-02 08:25:44',2378,'2025-09-02 10:34:04'),(38,'673602f0d14760402fbd033b_jsfb-40F663','OFFLINE','2025-09-02 08:39:25','2025-09-02 08:39:25',1835,'2025-09-02 10:34:04'),(39,'673602f0d14760402fbd033b_jsfb-EE4C3F','OFFLINE','2025-09-02 09:01:53','2025-09-02 09:01:53',4,'2025-09-02 10:34:04'),(40,'673602f0d14760402fbd033b_jsfb-9729D1','OFFLINE','2025-09-02 09:05:00','2025-09-02 09:05:00',245,'2025-09-02 10:34:04'),(41,'673602f0d14760402fbd033b_jsfb-9729D1','OFFLINE','2025-09-02 09:05:04','2025-09-02 09:05:04',2,'2025-09-02 10:34:04'),(42,'673602f0d14760402fbd033b_jsfb-960460','OFFLINE','2025-09-02 09:10:25','2025-09-02 09:10:25',1842,'2025-09-02 10:34:04'),(43,'673602f0d14760402fbd033b_jsfb-640DDE','OFFLINE','2025-09-02 09:28:42','2025-09-02 09:28:42',586,'2025-09-02 10:34:04'),(44,'673602f0d14760402fbd033b_jsfb-2211BF','OFFLINE','2025-09-02 09:34:33','2025-09-02 09:34:33',305,'2025-09-02 10:34:04'),(45,'673602f0d14760402fbd033b_jsfb-2211BF','OFFLINE','2025-09-02 09:35:34','2025-09-02 09:35:34',0,'2025-09-02 10:34:04'),(46,'673602f0d14760402fbd033b_jsfb-35E8F3','OFFLINE','2025-09-02 09:38:02','2025-09-02 09:38:02',850,'2025-09-02 10:34:04'),(47,'673602f0d14760402fbd033b_jsfb-35E8F3','OFFLINE','2025-09-02 09:38:15','2025-09-02 09:38:15',11,'2025-09-02 10:34:04'),(48,'673602f0d14760402fbd033b_jsfb-2211BF','OFFLINE','2025-09-02 09:39:22','2025-09-02 09:39:22',-1,'2025-09-02 10:34:04'),(49,'673602f0d14760402fbd033b_jsfb-2211BF','OFFLINE','2025-09-02 09:39:23','2025-09-02 09:39:23',0,'2025-09-02 10:34:04'),(50,'673602f0d14760402fbd033b_jsfb-2211BF','OFFLINE','2025-09-02 09:39:25','2025-09-02 09:39:25',0,'2025-09-02 10:34:04'),(51,'673602f0d14760402fbd033b_jsfb-2211BF','OFFLINE','2025-09-02 09:39:49','2025-09-02 09:39:49',0,'2025-09-02 10:34:05'),(52,'673602f0d14760402fbd033b_jsfb-2211BF','OFFLINE','2025-09-02 09:39:53','2025-09-02 09:39:53',2,'2025-09-02 10:34:05'),(53,'673602f0d14760402fbd033b_jsfb-2211BF','OFFLINE','2025-09-02 09:43:48','2025-09-02 09:43:48',73,'2025-09-02 10:34:05'),(54,'673602f0d14760402fbd033b_jsfb-CAC156','OFFLINE','2025-09-02 09:49:00','2025-09-02 09:49:00',-5,'2025-09-02 10:34:05'),(55,'673602f0d14760402fbd033b_jsfb-F90990','OFFLINE','2025-09-02 09:50:23','2025-09-02 09:50:23',236,'2025-09-02 10:34:05'),(56,'673602f0d14760402fbd033b_jsfb-F90990','OFFLINE','2025-09-02 09:50:25','2025-09-02 09:50:25',-2,'2025-09-02 10:34:05'),(57,'673602f0d14760402fbd033b_jsfb-216A14','OFFLINE','2025-09-02 09:52:49','2025-09-02 09:52:49',60,'2025-09-02 10:34:05'),(58,'673602f0d14760402fbd033b_jsfb-A1AFEF','OFFLINE','2025-09-02 09:55:18','2025-09-02 09:55:18',2629,'2025-09-02 10:34:05'),(59,'673602f0d14760402fbd033b_jsfb-A1AFEF','OFFLINE','2025-09-02 09:56:27','2025-09-02 09:56:27',2698,'2025-09-02 10:34:05'),(60,'673602f0d14760402fbd033b_jsfb-F90990','OFFLINE','2025-09-02 09:57:57','2025-09-02 09:57:57',360,'2025-09-02 10:34:05'),(61,'673602f0d14760402fbd033b_jsfb-F90990','OFFLINE','2025-09-02 09:57:58','2025-09-02 09:57:58',0,'2025-09-02 10:34:05'),(62,'673602f0d14760402fbd033b_jsfb-E20A6A','OFFLINE','2025-09-02 10:00:25','2025-09-02 10:00:25',3460,'2025-09-02 10:34:05'),(63,'673602f0d14760402fbd033b_jsfb-F90990','OFFLINE','2025-09-02 10:01:11','2025-09-02 10:01:11',-1,'2025-09-02 10:34:05'),(64,'673602f0d14760402fbd033b_jsfb-F90990','OFFLINE','2025-09-02 10:01:15','2025-09-02 10:01:15',3,'2025-09-02 10:34:05'),(65,'673602f0d14760402fbd033b_jsfb-39D61A','OFFLINE','2025-09-02 10:02:36','2025-09-02 10:02:36',106,'2025-09-02 10:34:05'),(66,'673602f0d14760402fbd033b_jsfb-39D61A','OFFLINE','2025-09-02 10:02:41','2025-09-02 10:02:41',111,'2025-09-02 10:34:05'),(67,'673602f0d14760402fbd033b_jsfb-AA0207','OFFLINE','2025-09-02 10:03:22','2025-09-02 10:03:22',30,'2025-09-02 10:34:05'),(68,'673602f0d14760402fbd033b_jsfb-A1AFEF','OFFLINE','2025-09-02 10:03:30','2025-09-02 10:03:30',275,'2025-09-02 10:34:05'),(69,'673602f0d14760402fbd033b_jsfb-A1AFEF','OFFLINE','2025-09-02 10:03:31','2025-09-02 10:03:31',0,'2025-09-02 10:34:05'),(70,'673602f0d14760402fbd033b_jsfb-A1AFEF','OFFLINE','2025-09-02 10:05:38','2025-09-02 10:05:38',-2,'2025-09-02 10:34:05'),(71,'673602f0d14760402fbd033b_jsfb-A1AFEF','OFFLINE','2025-09-02 10:05:39','2025-09-02 10:05:39',-1,'2025-09-02 10:34:05'),(72,'673602f0d14760402fbd033b_jsfb-EED4E5','OFFLINE','2025-09-02 10:06:21','2025-09-02 10:06:21',157,'2025-09-02 10:34:05'),(73,'673602f0d14760402fbd033b_jsfb-AA0207','OFFLINE','2025-09-02 10:06:56','2025-09-02 10:06:56',207,'2025-09-02 10:34:05'),(74,'673602f0d14760402fbd033b_jsfb-AA0207','OFFLINE','2025-09-02 10:06:57','2025-09-02 10:06:57',208,'2025-09-02 10:34:05'),(75,'673602f0d14760402fbd033b_jsfb-AA0207','OFFLINE','2025-09-02 10:07:00','2025-09-02 10:07:00',2,'2025-09-02 10:34:05'),(76,'673602f0d14760402fbd033b_jsfb-9729D1','OFFLINE','2025-09-02 10:11:15','2025-09-02 10:11:15',60,'2025-09-02 10:34:05'),(77,'673602f0d14760402fbd033b_jsfb-AE30E0','OFFLINE','2025-09-02 10:30:49','2025-09-02 10:30:49',8511,'2025-09-02 10:34:05'),(78,'673602f0d14760402fbd033b_jsfb-EED4E5','OFFLINE','2025-09-02 10:34:33','2025-09-02 10:34:33',340,'2025-09-02 10:34:33'),(79,'673602f0d14760402fbd033b_jsfb-2B6870','OFFLINE','2025-09-02 10:38:04','2025-09-02 10:38:04',143,'2025-09-02 10:38:05'); +/*!40000 ALTER TABLE `device_status_log` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `massage_task` +-- + +DROP TABLE IF EXISTS `massage_task`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `massage_task` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `device_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备ID', + `device_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备名称(冗余存储)', + `product_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品型号(冗余存储)', + `head_type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '按摩头类型', + `body_part` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '按摩部位', + `task_time` int(11) NOT NULL COMMENT '本次按摩持续时间(秒)', + `start_time` datetime NOT NULL COMMENT '开始时间', + `end_time` datetime NOT NULL COMMENT '结束时间', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + PRIMARY KEY (`id`), + KEY `idx_device_id` (`device_id`), + KEY `idx_start_time` (`start_time`), + KEY `idx_end_time` (`end_time`), + KEY `idx_device_time` (`device_id`,`start_time`) +) ENGINE=InnoDB AUTO_INCREMENT=536 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='按摩任务记录表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `massage_task` +-- + +LOCK TABLES `massage_task` WRITE; +/*!40000 ALTER TABLE `massage_task` DISABLE KEYS */; +INSERT INTO `massage_task` VALUES (1,'673602f0d14760402fbd033b_jsfb-DA',NULL,NULL,'shockwave','back',1800,'2025-09-01 17:16:55','2025-09-01 17:46:55',NULL),(2,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 17:28:04','2025-09-01 17:58:04',NULL),(3,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 17:37:44','2025-09-01 18:07:44',NULL),(4,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 17:54:04','2025-09-01 18:24:04',NULL),(5,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 17:58:29','2025-09-01 18:28:29','2025-09-01 18:28:37'),(6,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 19:08:29','2025-09-01 19:38:29','2025-09-01 19:38:30'),(7,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 19:10:05','2025-09-01 19:40:05','2025-09-01 19:40:06'),(8,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 19:10:15','2025-09-01 19:40:15','2025-09-01 19:40:16'),(9,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 19:11:12','2025-09-01 19:41:12','2025-09-01 19:41:12'),(10,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 19:11:22','2025-09-01 19:41:22','2025-09-01 19:41:22'),(11,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 19:11:32','2025-09-01 19:41:32','2025-09-01 19:41:32'),(12,'673602f0d14760402fbd033b_jsfb-DA','jsfb-DA',NULL,'shockwave','back',1800,'2025-09-01 19:11:42','2025-09-01 19:41:42','2025-09-01 19:41:42'),(13,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','finger','back',911,'2025-05-16 15:22:09','2025-05-16 15:37:20','2025-05-16 15:37:19'),(14,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','roller','leg',252,'2025-05-16 15:39:56','2025-05-16 15:44:08','2025-05-16 15:44:07'),(15,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','shockwave','back',118,'2025-05-16 16:04:51','2025-05-16 16:06:49','2025-05-16 16:06:52'),(16,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',206,'2025-05-16 16:11:00','2025-05-16 16:14:26','2025-05-16 16:14:25'),(17,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',38,'2025-05-16 16:16:41','2025-05-16 16:17:19','2025-05-16 16:17:18'),(18,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',62,'2025-05-16 16:18:10','2025-05-16 16:19:12','2025-05-16 16:19:11'),(19,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',123,'2025-05-16 16:20:22','2025-05-16 16:22:25','2025-05-16 16:22:25'),(20,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',46,'2025-05-16 16:23:17','2025-05-16 16:24:03','2025-05-16 16:24:02'),(21,'673602f0d14760402fbd033b_jsfb-test','jsfb-test','RS-LL-X1','finger','back',16,'2025-05-16 16:28:56','2025-05-16 16:29:12','2025-05-16 16:29:11'),(22,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','shockwave','back',138,'2025-05-16 16:33:54','2025-05-16 16:36:12','2025-05-16 16:35:59'),(23,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','shockwave','back',52,'2025-05-16 16:37:02','2025-05-16 16:37:54','2025-05-16 16:37:41'),(24,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','shockwave','back',250,'2025-05-16 16:40:10','2025-05-16 16:44:20','2025-05-16 16:44:08'),(25,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','roller','back',923,'2025-05-16 16:29:51','2025-05-16 16:45:14','2025-05-16 16:45:14'),(26,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','shockwave','back',78,'2025-05-16 16:45:17','2025-05-16 16:46:35','2025-05-16 16:46:23'),(27,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','shockwave','back',19,'2025-05-16 16:58:33','2025-05-16 16:58:52','2025-05-16 16:58:40'),(28,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','shockwave','back',138,'2025-05-16 16:59:43','2025-05-16 17:02:01','2025-05-16 17:01:48'),(29,'673602f0d14760402fbd033b_jsfb-CAC186','jsfb-CAC186','RS-LL-X1','shockwave','back',118,'2025-05-16 17:06:52','2025-05-16 17:08:50','2025-05-16 17:09:14'),(30,'673602f0d14760402fbd033b_jsfb-CAC186','jsfb-CAC186','RS-LL-X1','shockwave','back',69,'2025-05-16 17:10:46','2025-05-16 17:11:55','2025-05-16 17:12:19'),(31,'673602f0d14760402fbd033b_jsfb-CAC186','jsfb-CAC186','RS-LL-X1','shockwave','back',52,'2025-05-16 17:12:53','2025-05-16 17:13:45','2025-05-16 17:14:08'),(32,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','shockwave','back',747,'2025-05-16 17:02:52','2025-05-16 17:15:19','2025-05-16 17:15:07'),(33,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','roller','leg',920,'2025-05-16 17:24:47','2025-05-16 17:40:07','2025-05-16 17:40:55'),(34,'673602f0d14760402fbd033b_jsfb-CAC186','jsfb-CAC186','RS-LL-X1','shockwave','back',45,'2025-05-16 17:44:56','2025-05-16 17:45:41','2025-05-16 17:46:05'),(35,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','back',1853,'2025-05-16 21:33:46','2025-05-16 22:04:39','2025-05-16 22:04:39'),(36,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','back',939,'2025-05-16 22:11:42','2025-05-16 22:27:21','2025-05-16 22:27:20'),(37,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','back',1060,'2025-05-16 22:30:13','2025-05-16 22:47:53','2025-05-16 22:47:52'),(38,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','finger','back',53,'2025-05-17 11:11:37','2025-05-17 11:12:30','2025-05-17 11:12:26'),(39,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','shockwave','back',169,'2025-05-17 11:14:00','2025-05-17 11:16:49','2025-05-17 11:16:45'),(40,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','finger','shoulder',234,'2025-05-17 13:15:08','2025-05-17 13:19:02','2025-05-17 13:18:58'),(41,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','shockwave','shoulder',473,'2025-05-17 13:29:06','2025-05-17 13:36:59','2025-05-17 13:36:55'),(42,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','finger','shoulder',336,'2025-05-17 13:35:01','2025-05-17 13:40:37','2025-05-17 13:40:36'),(43,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','thermotherapy','back',17,'2025-05-17 13:45:27','2025-05-17 13:45:44','2025-05-17 13:45:40'),(44,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','thermotherapy','back',35,'2025-05-17 13:49:57','2025-05-17 13:50:32','2025-05-17 13:50:28'),(45,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','finger','shoulder',632,'2025-05-17 13:42:36','2025-05-17 13:53:08','2025-05-17 13:53:07'),(46,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','thermotherapy','back',15,'2025-05-17 14:48:54','2025-05-17 14:49:09','2025-05-17 14:49:05'),(47,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','thermotherapy','back',15,'2025-05-17 14:57:19','2025-05-17 14:57:34','2025-05-17 14:57:30'),(48,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','thermotherapy','back',20,'2025-05-17 15:08:01','2025-05-17 15:08:21','2025-05-17 15:08:18'),(49,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','thermotherapy','back',16,'2025-05-17 15:13:35','2025-05-17 15:13:51','2025-05-17 15:13:47'),(50,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','back',18,'2025-05-17 15:25:47','2025-05-17 15:26:05','2025-05-17 15:26:01'),(51,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','back',54,'2025-05-17 15:57:11','2025-05-17 15:58:05','2025-05-17 15:58:01'),(52,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','shockwave','back',956,'2025-05-17 16:04:44','2025-05-17 16:20:40','2025-05-17 16:20:36'),(53,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','shockwave','back',948,'2025-05-17 16:25:09','2025-05-17 16:40:57','2025-05-17 16:40:53'),(54,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','shoulder',1827,'2025-05-17 17:59:55','2025-05-17 18:30:22','2025-05-17 18:30:22'),(55,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','shockwave','back',944,'2025-05-17 18:14:57','2025-05-17 18:30:41','2025-05-17 18:30:36'),(56,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','back',929,'2025-05-17 18:31:44','2025-05-17 18:47:13','2025-05-17 18:47:12'),(57,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','shockwave','waist',938,'2025-05-17 18:32:07','2025-05-17 18:47:45','2025-05-17 18:47:41'),(58,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','waist',778,'2025-05-17 18:50:00','2025-05-17 19:02:58','2025-05-17 19:02:57'),(59,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','back',799,'2025-05-17 19:08:01','2025-05-17 19:21:20','2025-05-17 19:21:18'),(60,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','roller','waist',781,'2025-05-17 19:11:11','2025-05-17 19:24:12','2025-05-17 19:24:11'),(61,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','waist',804,'2025-05-17 19:30:25','2025-05-17 19:43:49','2025-05-17 19:43:48'),(62,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','back',921,'2025-05-17 19:53:55','2025-05-17 20:09:16','2025-05-17 20:09:14'),(63,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','waist',474,'2025-05-17 20:12:13','2025-05-17 20:20:07','2025-05-17 20:20:05'),(64,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',952,'2025-05-17 20:26:00','2025-05-17 20:41:52','2025-05-17 20:41:50'),(65,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',3,'2025-05-17 21:14:39','2025-05-17 21:14:42','2025-05-17 21:14:43'),(66,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',222,'2025-05-17 21:31:53','2025-05-17 21:35:35','2025-05-17 21:35:33'),(67,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',4,'2025-05-17 21:38:15','2025-05-17 21:38:19','2025-05-17 21:38:19'),(68,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',4,'2025-05-17 21:44:53','2025-05-17 21:44:57','2025-05-17 21:44:57'),(69,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',25,'2025-05-17 21:53:39','2025-05-17 21:54:04','2025-05-17 21:54:04'),(70,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1838,'2025-05-17 21:39:05','2025-05-17 22:09:43','2025-05-17 22:09:41'),(71,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',24,'2025-05-17 22:10:57','2025-05-17 22:11:21','2025-05-17 22:11:21'),(72,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','waist',84,'2025-05-17 22:23:19','2025-05-17 22:24:43','2025-05-17 22:24:41'),(73,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',30,'2025-05-17 22:33:25','2025-05-17 22:33:55','2025-05-17 22:33:55'),(74,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',230,'2025-05-18 02:09:28','2025-05-18 02:13:18','2025-05-18 02:13:18'),(75,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',21,'2025-05-18 02:18:49','2025-05-18 02:19:10','2025-05-18 02:19:10'),(76,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',110,'2025-05-18 02:46:09','2025-05-18 02:47:59','2025-05-18 02:47:59'),(77,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',26,'2025-05-18 02:49:23','2025-05-18 02:49:49','2025-05-18 02:49:49'),(78,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',947,'2025-05-18 02:52:26','2025-05-18 03:08:13','2025-05-18 03:08:13'),(79,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',30,'2025-05-18 03:09:38','2025-05-18 03:10:08','2025-05-18 03:10:09'),(80,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',123,'2025-05-18 03:11:15','2025-05-18 03:13:18','2025-05-18 03:13:18'),(81,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','finger','back',73,'2025-05-18 03:15:47','2025-05-18 03:17:00','2025-05-18 03:17:00'),(82,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',20,'2025-05-18 03:18:50','2025-05-18 03:19:10','2025-05-18 03:19:10'),(83,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','shockwave','back',943,'2025-05-18 09:38:36','2025-05-18 09:54:19','2025-05-18 09:54:15'),(84,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','belly',945,'2025-05-18 10:10:17','2025-05-18 10:26:02','2025-05-18 10:26:00'),(85,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','belly',3627,'2025-05-18 10:29:09','2025-05-18 11:29:36','2025-05-18 11:29:35'),(86,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1829,'2025-05-18 11:17:29','2025-05-18 11:47:58','2025-05-18 11:47:56'),(87,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','back',1842,'2025-05-18 12:39:06','2025-05-18 13:09:48','2025-05-18 13:09:46'),(88,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','back',939,'2025-05-18 13:15:43','2025-05-18 13:31:22','2025-05-18 13:31:20'),(89,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','back',945,'2025-05-18 13:35:53','2025-05-18 13:51:38','2025-05-18 13:51:36'),(90,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','roller','back',16,'2025-05-18 13:53:57','2025-05-18 13:54:13','2025-05-18 13:54:11'),(91,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','thermotherapy','back',552,'2025-05-18 13:56:07','2025-05-18 14:05:19','2025-05-18 14:05:17'),(92,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','back',935,'2025-05-18 13:55:11','2025-05-18 14:10:46','2025-05-18 14:10:42'),(93,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','roller','back',290,'2025-05-18 14:07:22','2025-05-18 14:12:12','2025-05-18 14:12:10'),(94,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','waist',692,'2025-05-18 14:18:30','2025-05-18 14:30:02','2025-05-18 14:30:00'),(95,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','waist',150,'2025-05-18 14:31:54','2025-05-18 14:34:24','2025-05-18 14:34:22'),(96,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','back',934,'2025-05-18 14:27:39','2025-05-18 14:43:13','2025-05-18 14:43:11'),(97,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','belly',1857,'2025-05-18 14:14:36','2025-05-18 14:45:33','2025-05-18 14:45:29'),(98,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',959,'2025-05-18 14:41:34','2025-05-18 14:57:33','2025-05-18 14:57:31'),(99,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','shockwave','back',921,'2025-05-18 14:45:48','2025-05-18 15:01:09','2025-05-18 15:01:07'),(100,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','back',131,'2025-05-18 15:15:02','2025-05-18 15:17:13','2025-05-18 15:17:08'),(101,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','roller','back',925,'2025-05-18 15:03:00','2025-05-18 15:18:25','2025-05-18 15:18:23'),(102,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',922,'2025-05-18 15:09:47','2025-05-18 15:25:09','2025-05-18 15:25:07'),(103,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','back',22,'2025-05-18 15:38:31','2025-05-18 15:38:53','2025-05-18 15:38:49'),(104,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','thermotherapy','back',936,'2025-05-18 15:24:51','2025-05-18 15:40:27','2025-05-18 15:40:25'),(105,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','waist',516,'2025-05-18 15:41:46','2025-05-18 15:50:22','2025-05-18 15:50:17'),(106,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1853,'2025-05-18 15:28:59','2025-05-18 15:59:52','2025-05-18 15:59:50'),(107,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','finger','shoulder',1329,'2025-05-18 15:52:14','2025-05-18 16:14:23','2025-05-18 16:14:18'),(108,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1490,'2025-05-18 16:09:26','2025-05-18 16:34:16','2025-05-18 16:34:14'),(109,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','belly',1506,'2025-05-18 16:19:57','2025-05-18 16:45:03','2025-05-18 16:44:59'),(110,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1824,'2025-05-18 16:42:14','2025-05-18 17:12:38','2025-05-18 17:12:36'),(111,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','belly',1828,'2025-05-18 16:56:57','2025-05-18 17:27:25','2025-05-18 17:27:23'),(112,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','waist',931,'2025-05-18 18:37:33','2025-05-18 18:53:04','2025-05-18 18:53:05'),(113,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','stone','belly',950,'2025-05-18 19:00:07','2025-05-18 19:15:57','2025-05-18 19:15:57'),(114,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','back',926,'2025-05-18 19:03:34','2025-05-18 19:19:00','2025-05-18 19:18:56'),(115,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','stone','belly',938,'2025-05-18 19:22:02','2025-05-18 19:37:40','2025-05-18 19:37:41'),(116,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','waist',935,'2025-05-18 19:22:48','2025-05-18 19:38:23','2025-05-18 19:38:19'),(117,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','shoulder',1838,'2025-05-18 19:48:50','2025-05-18 20:19:28','2025-05-18 20:19:28'),(118,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',2742,'2025-05-18 20:00:09','2025-05-18 20:45:51','2025-05-18 20:45:49'),(119,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','stone','belly',1839,'2025-05-18 20:24:14','2025-05-18 20:54:53','2025-05-18 20:54:53'),(120,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','shockwave','back',928,'2025-05-19 08:54:15','2025-05-19 09:09:43','2025-05-19 09:09:41'),(121,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','thermotherapy','back',939,'2025-05-19 09:13:14','2025-05-19 09:28:53','2025-05-19 09:28:51'),(122,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','roller','back',55,'2025-05-19 09:28:52','2025-05-19 09:29:47','2025-05-19 09:29:45'),(123,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','back',922,'2025-05-19 09:33:32','2025-05-19 09:48:54','2025-05-19 09:48:52'),(124,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',602,'2025-05-19 09:42:06','2025-05-19 09:52:08','2025-05-19 09:52:06'),(125,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',751,'2025-05-19 09:54:15','2025-05-19 10:06:46','2025-05-19 10:06:44'),(126,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','waist',927,'2025-05-19 10:08:47','2025-05-19 10:24:14','2025-05-19 10:24:12'),(127,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','thermotherapy','back',944,'2025-05-19 10:27:33','2025-05-19 10:43:17','2025-05-19 10:43:15'),(128,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','thermotherapy','waist',57,'2025-05-19 10:52:41','2025-05-19 10:53:38','2025-05-19 10:53:38'),(129,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','back',179,'2025-05-19 11:05:56','2025-05-19 11:08:55','2025-05-19 11:08:53'),(130,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','stone','back',35,'2025-05-19 11:09:23','2025-05-19 11:09:58','2025-05-19 11:09:47'),(131,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','thermotherapy','waist',943,'2025-05-19 10:56:22','2025-05-19 11:12:05','2025-05-19 11:12:05'),(132,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','thermotherapy','waist',938,'2025-05-19 11:03:12','2025-05-19 11:18:50','2025-05-19 11:18:48'),(133,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','shockwave','waist',56,'2025-05-19 11:20:46','2025-05-19 11:21:42','2025-05-19 11:21:42'),(134,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','thermotherapy','back',738,'2025-05-19 11:21:23','2025-05-19 11:33:41','2025-05-19 11:33:39'),(135,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','stone','waist',943,'2025-05-19 11:28:20','2025-05-19 11:44:03','2025-05-19 11:44:03'),(136,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',33,'2025-05-19 11:52:15','2025-05-19 11:52:48','2025-05-19 11:52:48'),(137,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',17,'2025-05-19 11:54:04','2025-05-19 11:54:21','2025-05-19 11:54:21'),(138,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','thermotherapy','back',189,'2025-05-19 11:55:30','2025-05-19 11:58:39','2025-05-19 11:58:39'),(139,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','thermotherapy','back',637,'2025-05-19 11:49:18','2025-05-19 11:59:55','2025-05-19 11:59:53'),(140,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','thermotherapy','back',34,'2025-05-19 11:59:56','2025-05-19 12:00:30','2025-05-19 12:00:30'),(141,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','shockwave','back',44,'2025-05-19 12:01:30','2025-05-19 12:02:14','2025-05-19 12:02:14'),(142,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',2740,'2025-05-19 11:17:44','2025-05-19 12:03:24','2025-05-19 12:03:22'),(143,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','back',1838,'2025-05-19 12:10:53','2025-05-19 12:41:31','2025-05-19 12:41:29'),(144,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','thermotherapy','belly',1504,'2025-05-19 13:37:21','2025-05-19 14:02:25','2025-05-19 14:02:23'),(145,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','stone','back',892,'2025-05-19 13:49:16','2025-05-19 14:04:08','2025-05-19 14:04:08'),(146,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','thermotherapy','belly',180,'2025-05-19 14:04:21','2025-05-19 14:07:21','2025-05-19 14:07:20'),(147,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1836,'2025-05-19 13:39:34','2025-05-19 14:10:10','2025-05-19 14:10:08'),(148,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','thermotherapy','back',136,'2025-05-19 14:08:35','2025-05-19 14:10:51','2025-05-19 14:10:51'),(149,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','thermotherapy','belly',1124,'2025-05-19 14:10:12','2025-05-19 14:28:56','2025-05-19 14:28:54'),(150,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','thermotherapy','back',594,'2025-05-19 14:24:17','2025-05-19 14:34:11','2025-05-19 14:34:11'),(151,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','waist',1844,'2025-05-19 14:15:15','2025-05-19 14:45:59','2025-05-19 14:45:57'),(152,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','roller','back',1026,'2025-05-19 14:38:38','2025-05-19 14:55:44','2025-05-19 14:55:44'),(153,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','thermotherapy','back',1626,'2025-05-19 14:33:45','2025-05-19 15:00:51','2025-05-19 15:00:49'),(154,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','roller','back',918,'2025-05-19 14:57:30','2025-05-19 15:12:48','2025-05-19 15:12:48'),(155,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','ball','back',941,'2025-05-19 14:59:35','2025-05-19 15:15:16','2025-05-19 15:15:14'),(156,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','thermotherapy','back',1324,'2025-05-19 15:02:52','2025-05-19 15:24:56','2025-05-19 15:24:55'),(157,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',53,'2025-05-19 15:26:28','2025-05-19 15:27:21','2025-05-19 15:27:19'),(158,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','shoulder',69,'2025-05-19 15:28:24','2025-05-19 15:29:33','2025-05-19 15:29:31'),(159,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','roller','back',920,'2025-05-19 15:15:37','2025-05-19 15:30:57','2025-05-19 15:30:58'),(160,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',45,'2025-05-19 15:30:47','2025-05-19 15:31:32','2025-05-19 15:31:30'),(161,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','back',926,'2025-05-19 15:18:42','2025-05-19 15:34:08','2025-05-19 15:34:06'),(162,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',100,'2025-05-19 15:33:09','2025-05-19 15:34:49','2025-05-19 15:34:47'),(163,'673602f0d14760402fbd033b_jsfb-CAC186','jsfb-CAC186','RS-LL-X1','shockwave','back',48,'2025-05-19 15:38:22','2025-05-19 15:39:10','2025-05-19 15:39:38'),(164,'673602f0d14760402fbd033b_jsfb-CAC186','jsfb-CAC186','RS-LL-X1','shockwave','back',44,'2025-05-19 15:40:31','2025-05-19 15:41:15','2025-05-19 15:41:43'),(165,'673602f0d14760402fbd033b_jsfb-CAC186','jsfb-CAC186','RS-LL-X1','shockwave','back',43,'2025-05-19 15:42:14','2025-05-19 15:42:57','2025-05-19 15:43:25'),(166,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','back',716,'2025-05-19 15:36:07','2025-05-19 15:48:03','2025-05-19 15:48:01'),(167,'673602f0d14760402fbd033b_jsfb-6E04FA','jsfb-6E04FA','RS-LL-X1','finger','back',354,'2025-05-19 15:43:05','2025-05-19 15:48:59','2025-05-19 15:48:59'),(168,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','roller','back',917,'2025-05-19 15:36:49','2025-05-19 15:52:06','2025-05-19 15:52:06'),(169,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',50,'2025-05-19 15:55:40','2025-05-19 15:56:30','2025-05-19 15:56:28'),(170,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','belly',2723,'2025-05-19 15:11:57','2025-05-19 15:57:20','2025-05-19 15:57:18'),(171,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','roller','back',921,'2025-05-19 15:55:16','2025-05-19 16:10:37','2025-05-19 16:10:37'),(172,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',406,'2025-05-19 16:09:30','2025-05-19 16:16:16','2025-05-19 16:16:14'),(173,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','shoulder',1234,'2025-05-19 15:57:53','2025-05-19 16:18:27','2025-05-19 16:18:25'),(174,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','stone','back',870,'2025-05-19 16:18:19','2025-05-19 16:32:49','2025-05-19 16:32:47'),(175,'673602f0d14760402fbd033b_jsfb-40F663','jsfb-40F663','RS-LL-X1','roller','back',920,'2025-05-19 16:17:52','2025-05-19 16:33:12','2025-05-19 16:33:12'),(176,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','waist',834,'2025-05-19 16:21:50','2025-05-19 16:35:44','2025-05-19 16:35:43'),(177,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','waist',46,'2025-05-19 16:39:31','2025-05-19 16:40:17','2025-05-19 16:40:15'),(178,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','waist',423,'2025-05-19 16:43:29','2025-05-19 16:50:32','2025-05-19 16:50:31'),(179,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','stone','back',947,'2025-05-19 16:36:07','2025-05-19 16:51:54','2025-05-19 16:51:53'),(180,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','stone','back',1991,'2025-05-19 16:59:00','2025-05-19 17:32:11','2025-05-19 17:32:09'),(181,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','finger','back',42,'2025-05-19 17:37:50','2025-05-19 17:38:32','2025-05-19 17:38:33'),(182,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','back',930,'2025-05-19 17:28:47','2025-05-19 17:44:17','2025-05-19 17:44:16'),(183,'673602f0d14760402fbd033b_jsfb-CAC186','jsfb-CAC186','RS-LL-X1','shockwave','back',714,'2025-05-19 17:35:57','2025-05-19 17:47:51','2025-05-19 17:48:19'),(184,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','back',157,'2025-05-19 17:46:39','2025-05-19 17:49:16','2025-05-19 17:49:14'),(185,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',2738,'2025-05-19 18:49:28','2025-05-19 19:35:06','2025-05-19 19:35:04'),(186,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','finger','back',7226,'2025-05-19 17:39:59','2025-05-19 19:40:25','2025-05-19 19:40:25'),(187,'673602f0d14760402fbd033b_jsfb-5EC2B8','jsfb-5EC2B8','RS-LL-X1','stone','back',394,'2025-05-19 19:38:32','2025-05-19 19:45:06','2025-05-19 19:45:03'),(188,'673602f0d14760402fbd033b_jsfb-5EC2B8','jsfb-5EC2B8','RS-LL-X1','stone','belly',935,'2025-05-19 20:57:08','2025-05-19 21:12:43','2025-05-19 21:12:43'),(189,'673602f0d14760402fbd033b_jsfb-5EC2B8','jsfb-5EC2B8','RS-LL-X1','stone','belly',520,'2025-05-19 21:15:32','2025-05-19 21:24:12','2025-05-19 21:24:12'),(190,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',2752,'2025-05-19 20:51:31','2025-05-19 21:37:23','2025-05-19 21:37:21'),(191,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','back',773,'2025-05-19 21:41:58','2025-05-19 21:54:51','2025-05-19 21:54:49'),(192,'673602f0d14760402fbd033b_jsfb-5EC2B8','jsfb-5EC2B8','RS-LL-X1','stone','belly',960,'2025-05-19 21:41:21','2025-05-19 21:57:21','2025-05-19 21:57:21'),(193,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','back',705,'2025-05-19 21:58:50','2025-05-19 22:10:35','2025-05-19 22:10:33'),(194,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','thermotherapy','back',942,'2025-05-19 22:26:02','2025-05-19 22:41:44','2025-05-19 22:41:42'),(195,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','thermotherapy','back',1836,'2025-05-19 22:43:16','2025-05-19 23:13:52','2025-05-19 23:13:50'),(196,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','thermotherapy','back',948,'2025-05-19 23:24:40','2025-05-19 23:40:28','2025-05-19 23:40:26'),(197,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','shockwave','shoulder',919,'2025-05-19 23:45:23','2025-05-20 00:00:42','2025-05-20 00:00:40'),(198,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',370,'2025-05-20 09:30:32','2025-05-20 09:36:42','2025-05-20 09:36:42'),(199,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','back',70,'2025-05-20 09:39:48','2025-05-20 09:40:58','2025-05-20 09:40:56'),(200,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',65,'2025-05-20 09:49:52','2025-05-20 09:50:57','2025-05-20 09:50:56'),(201,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',927,'2025-05-20 09:38:23','2025-05-20 09:53:50','2025-05-20 09:53:50'),(202,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','thermotherapy','back',54,'2025-05-20 09:53:06','2025-05-20 09:54:00','2025-05-20 09:53:58'),(203,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',113,'2025-05-20 10:10:13','2025-05-20 10:12:06','2025-05-20 10:12:06'),(204,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','stone','back',59,'2025-05-20 10:16:30','2025-05-20 10:17:29','2025-05-20 10:17:27'),(205,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','shockwave','back',115,'2025-05-20 10:19:48','2025-05-20 10:21:43','2025-05-20 10:21:43'),(206,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','shockwave','back',71,'2025-05-20 10:26:17','2025-05-20 10:27:28','2025-05-20 10:27:16'),(207,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','stone','back',56,'2025-05-20 10:29:35','2025-05-20 10:30:31','2025-05-20 10:30:19'),(208,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','thermotherapy','back',37,'2025-05-20 10:32:07','2025-05-20 10:32:44','2025-05-20 10:32:32'),(209,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','stone','back',926,'2025-05-20 10:18:29','2025-05-20 10:33:55','2025-05-20 10:33:53'),(210,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','back',1828,'2025-05-20 10:16:24','2025-05-20 10:46:52','2025-05-20 10:46:50'),(211,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','stone','back',110,'2025-05-20 10:58:14','2025-05-20 11:00:04','2025-05-20 11:00:04'),(212,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',50,'2025-05-20 11:01:27','2025-05-20 11:02:17','2025-05-20 11:02:17'),(213,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','shockwave','back',933,'2025-05-20 11:21:44','2025-05-20 11:37:17','2025-05-20 11:37:16'),(214,'673602f0d14760402fbd033b_jsfb-1B9894','jsfb-1B9894','RS-LL-X1','stone','waist',937,'2025-05-20 11:35:31','2025-05-20 11:51:08','2025-05-20 11:51:01'),(215,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','back',570,'2025-05-20 13:08:03','2025-05-20 13:17:33','2025-05-20 13:17:31'),(216,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','shockwave','shoulder',922,'2025-05-20 13:14:39','2025-05-20 13:30:01','2025-05-20 13:30:00'),(217,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','shockwave','shoulder',924,'2025-05-20 13:30:24','2025-05-20 13:45:48','2025-05-20 13:45:47'),(218,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',180,'2025-05-20 13:48:53','2025-05-20 13:51:53','2025-05-20 13:51:52'),(219,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','finger','back',7247,'2025-05-20 11:57:59','2025-05-20 13:58:46','2025-05-20 13:58:45'),(220,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','finger','back',216,'2025-05-20 14:00:40','2025-05-20 14:04:16','2025-05-20 14:04:15'),(221,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','finger','shoulder',1823,'2025-05-20 13:34:57','2025-05-20 14:05:20','2025-05-20 14:05:19'),(222,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','stone','back',219,'2025-05-20 14:05:26','2025-05-20 14:09:05','2025-05-20 14:09:05'),(223,'673602f0d14760402fbd033b_jsfb-6E04FA','jsfb-6E04FA','RS-LL-X1','heat','back',9704,'2025-05-20 11:45:14','2025-05-20 14:26:58','2025-05-20 14:26:59'),(224,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','finger','back',1337,'2025-05-20 14:10:17','2025-05-20 14:32:34','2025-05-20 14:32:33'),(225,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','finger','back',3365,'2025-05-20 13:39:43','2025-05-20 14:35:48','2025-05-20 14:35:55'),(226,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','finger','shoulder',1838,'2025-05-20 14:10:41','2025-05-20 14:41:19','2025-05-20 14:41:18'),(227,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','thermotherapy','back',157,'2025-05-20 14:44:35','2025-05-20 14:47:12','2025-05-20 14:47:11'),(228,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',70,'2025-05-20 14:48:13','2025-05-20 14:49:23','2025-05-20 14:49:23'),(229,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','thermotherapy','back',943,'2025-05-20 14:33:52','2025-05-20 14:49:35','2025-05-20 14:49:34'),(230,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','stone','back',934,'2025-05-20 14:37:45','2025-05-20 14:53:19','2025-05-20 14:53:26'),(231,'673602f0d14760402fbd033b_jsfb-CAC186','jsfb-CAC186','RS-LL-X1','shockwave','back',932,'2025-05-20 14:43:33','2025-05-20 14:59:05','2025-05-20 14:59:34'),(232,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',932,'2025-05-20 14:50:32','2025-05-20 15:06:04','2025-05-20 15:06:04'),(233,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','stone','back',941,'2025-05-20 14:56:23','2025-05-20 15:12:04','2025-05-20 15:12:11'),(234,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','thermotherapy','back',951,'2025-05-20 14:57:59','2025-05-20 15:13:50','2025-05-20 15:13:49'),(235,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','thermotherapy','belly',1873,'2025-05-20 14:46:33','2025-05-20 15:17:46','2025-05-20 15:17:45'),(236,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','finger','back',17,'2025-05-20 15:27:38','2025-05-20 15:27:55','2025-05-20 15:27:54'),(237,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','thermotherapy','back',342,'2025-05-20 15:28:53','2025-05-20 15:34:35','2025-05-20 15:34:34'),(238,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','thermotherapy','back',15,'2025-05-20 15:35:30','2025-05-20 15:35:45','2025-05-20 15:35:44'),(239,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','back',1173,'2025-05-20 15:22:18','2025-05-20 15:41:51','2025-05-20 15:41:49'),(240,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','stone','back',944,'2025-05-20 15:30:15','2025-05-20 15:45:59','2025-05-20 15:46:05'),(241,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','belly',1854,'2025-05-20 15:22:38','2025-05-20 15:53:32','2025-05-20 15:53:30'),(242,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',1837,'2025-05-20 15:24:23','2025-05-20 15:55:00','2025-05-20 15:55:00'),(243,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','thermotherapy','back',118,'2025-05-20 15:53:27','2025-05-20 15:55:25','2025-05-20 15:55:24'),(244,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','finger','back',48,'2025-05-20 16:05:22','2025-05-20 16:06:10','2025-05-20 16:06:09'),(245,'673602f0d14760402fbd033b_jsfb-CAC186','jsfb-CAC186','RS-LL-X1','shockwave','back',1687,'2025-05-20 15:39:12','2025-05-20 16:07:19','2025-05-20 16:07:48'),(246,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',323,'2025-05-20 16:03:21','2025-05-20 16:08:44','2025-05-20 16:08:44'),(247,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','finger','back',158,'2025-05-20 16:07:15','2025-05-20 16:09:53','2025-05-20 16:10:00'),(248,'673602f0d14760402fbd033b_jsfb-6E04FA','jsfb-6E04FA','RS-LL-X1','spheres','back',74,'2025-05-20 16:23:40','2025-05-20 16:24:54','2025-05-20 16:24:54'),(249,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',54,'2025-05-20 16:24:40','2025-05-20 16:25:34','2025-05-20 16:25:34'),(250,'673602f0d14760402fbd033b_jsfb-797BE1','jsfb-797BE1','RS-LL-X1','stone','back',952,'2025-05-20 16:09:53','2025-05-20 16:25:45','2025-05-20 16:25:44'),(251,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1846,'2025-05-20 15:58:45','2025-05-20 16:29:31','2025-05-20 16:29:29'),(252,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',42,'2025-05-20 16:29:14','2025-05-20 16:29:56','2025-05-20 16:29:56'),(253,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',78,'2025-05-20 16:37:53','2025-05-20 16:39:11','2025-05-20 16:39:11'),(254,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','shoulder',628,'2025-05-20 16:34:10','2025-05-20 16:44:38','2025-05-20 16:44:38'),(255,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1085,'2025-05-20 16:32:20','2025-05-20 16:50:25','2025-05-20 16:50:23'),(256,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',108,'2025-05-20 16:50:15','2025-05-20 16:52:03','2025-05-20 16:52:04'),(257,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','shoulder',432,'2025-05-20 16:46:11','2025-05-20 16:53:23','2025-05-20 16:53:23'),(258,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',447,'2025-05-20 16:53:49','2025-05-20 17:01:16','2025-05-20 17:01:16'),(259,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','finger','leg',946,'2025-05-20 16:46:02','2025-05-20 17:01:48','2025-05-20 17:01:47'),(260,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',120,'2025-05-20 17:03:24','2025-05-20 17:05:24','2025-05-20 17:05:24'),(261,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','back',1401,'2025-05-20 16:55:06','2025-05-20 17:18:27','2025-05-20 17:18:27'),(262,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',90,'2025-05-20 17:22:31','2025-05-20 17:24:01','2025-05-20 17:24:01'),(263,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','thermotherapy','back',50,'2025-05-20 17:27:51','2025-05-20 17:28:41','2025-05-20 17:28:51'),(264,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','finger','back',35,'2025-05-20 17:30:23','2025-05-20 17:30:58','2025-05-20 17:31:09'),(265,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','finger','leg',1820,'2025-05-20 17:02:57','2025-05-20 17:33:17','2025-05-20 17:33:16'),(266,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','stone','back',441,'2025-05-20 17:32:02','2025-05-20 17:39:23','2025-05-20 17:39:33'),(267,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1856,'2025-05-20 17:10:15','2025-05-20 17:41:11','2025-05-20 17:41:09'),(268,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',916,'2025-05-20 17:33:53','2025-05-20 17:49:09','2025-05-20 17:49:09'),(269,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','thermotherapy','back',229,'2025-05-20 17:56:01','2025-05-20 17:59:50','2025-05-20 18:00:00'),(270,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1850,'2025-05-20 17:43:58','2025-05-20 18:14:48','2025-05-20 18:14:46'),(271,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','stone','belly',3658,'2025-05-20 17:26:32','2025-05-20 18:27:30','2025-05-20 18:27:30'),(272,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','thermotherapy','back',952,'2025-05-20 18:18:35','2025-05-20 18:34:27','2025-05-20 18:34:37'),(273,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',943,'2025-05-20 18:26:48','2025-05-20 18:42:31','2025-05-20 18:42:32'),(274,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',74,'2025-05-20 18:45:46','2025-05-20 18:47:00','2025-05-20 18:47:00'),(275,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','finger','back',927,'2025-05-20 18:48:49','2025-05-20 19:04:16','2025-05-20 19:04:15'),(276,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',68,'2025-05-20 19:08:54','2025-05-20 19:10:02','2025-05-20 19:10:02'),(277,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',81,'2025-05-20 19:15:02','2025-05-20 19:16:23','2025-05-20 19:16:23'),(278,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','finger','back',110,'2025-05-20 19:31:07','2025-05-20 19:32:57','2025-05-20 19:32:57'),(279,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','waist',1838,'2025-05-20 19:06:55','2025-05-20 19:37:33','2025-05-20 19:37:31'),(280,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','thermotherapy','back',365,'2025-05-20 19:34:07','2025-05-20 19:40:12','2025-05-20 19:40:12'),(281,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','shockwave','back',90,'2025-05-20 19:47:53','2025-05-20 19:49:23','2025-05-20 19:49:23'),(282,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','back',1848,'2025-05-20 19:43:34','2025-05-20 20:14:22','2025-05-20 20:14:20'),(283,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','finger','back',7231,'2025-05-20 19:19:46','2025-05-20 21:20:17','2025-05-20 21:20:15'),(284,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1830,'2025-05-20 20:59:21','2025-05-20 21:29:51','2025-05-20 21:29:48'),(285,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','roller','back',7232,'2025-05-20 19:36:27','2025-05-20 21:36:59','2025-05-20 21:36:58'),(286,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','shockwave','back',7235,'2025-05-20 19:50:16','2025-05-20 21:50:51','2025-05-20 21:50:50'),(287,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','thermotherapy','back',70017,'2025-05-20 13:40:14','2025-05-21 09:07:11','2025-05-21 09:06:58'),(288,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','thermotherapy','back',506,'2025-05-21 09:08:51','2025-05-21 09:17:17','2025-05-21 09:17:04'),(289,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','shockwave','back',119,'2025-05-21 09:34:37','2025-05-21 09:36:36','2025-05-21 09:36:36'),(290,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','stone','back',53,'2025-05-21 09:40:30','2025-05-21 09:41:23','2025-05-21 09:41:22'),(291,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','thermotherapy','back',34,'2025-05-21 09:45:45','2025-05-21 09:46:19','2025-05-21 09:46:20'),(292,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','thermotherapy','back',2771,'2025-05-21 09:18:06','2025-05-21 10:04:17','2025-05-21 10:04:04'),(293,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','back',1857,'2025-05-21 09:37:16','2025-05-21 10:08:13','2025-05-21 10:08:13'),(294,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',4410,'2025-05-21 09:13:17','2025-05-21 10:26:47','2025-05-21 10:26:44'),(295,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','back',931,'2025-05-21 10:30:33','2025-05-21 10:46:04','2025-05-21 10:46:03'),(296,'673602f0d14760402fbd033b_jsfb-B9771F','jsfb-B9771F','RS-LL-X1','roller','back',5607,'2025-05-21 09:14:15','2025-05-21 10:47:42','2025-05-21 10:47:39'),(297,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','waist',2748,'2025-05-21 10:13:14','2025-05-21 10:59:02','2025-05-21 10:59:03'),(298,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','shockwave','shoulder',640,'2025-05-21 10:48:26','2025-05-21 10:59:06','2025-05-21 10:59:05'),(299,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',929,'2025-05-21 10:50:11','2025-05-21 11:05:40','2025-05-21 11:05:39'),(300,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','thermotherapy','back',7243,'2025-05-21 09:10:50','2025-05-21 11:11:33','2025-05-21 11:11:45'),(301,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','thermotherapy','back',3579,'2025-05-21 10:18:33','2025-05-21 11:18:12','2025-05-21 11:17:59'),(302,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','stone','back',1377,'2025-05-21 11:25:38','2025-05-21 11:48:35','2025-05-21 11:48:34'),(303,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',3635,'2025-05-21 11:03:12','2025-05-21 12:03:47','2025-05-21 12:03:46'),(304,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','shockwave','back',660,'2025-05-21 11:53:53','2025-05-21 12:04:53','2025-05-21 12:04:53'),(305,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','stone','belly',1840,'2025-05-21 11:51:31','2025-05-21 12:22:11','2025-05-21 12:22:12'),(306,'673602f0d14760402fbd033b_jsfb-642D43','jsfb-642D43','RS-LL-X1','thermotherapy','back',70,'2025-05-21 13:41:55','2025-05-21 13:43:05','2025-05-21 13:43:16'),(307,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','shockwave','back',928,'2025-05-21 14:07:37','2025-05-21 14:23:05','2025-05-21 14:23:05'),(308,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','stone','back',425,'2025-05-21 14:25:32','2025-05-21 14:32:37','2025-05-21 14:32:37'),(309,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','roller','back',1120,'2025-05-21 14:29:18','2025-05-21 14:47:58','2025-05-21 14:47:58'),(310,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',2730,'2025-05-21 14:04:52','2025-05-21 14:50:22','2025-05-21 14:50:21'),(311,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','thermotherapy','back',25,'2025-05-21 14:53:11','2025-05-21 14:53:36','2025-05-21 14:53:34'),(312,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','stone','back',533,'2025-05-21 14:51:12','2025-05-21 15:00:05','2025-05-21 15:00:05'),(313,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','thermotherapy','back',44,'2025-05-21 15:02:00','2025-05-21 15:02:44','2025-05-21 15:02:44'),(314,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','shockwave','back',43,'2025-05-21 15:04:16','2025-05-21 15:04:59','2025-05-21 15:04:47'),(315,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',53,'2025-05-21 15:10:24','2025-05-21 15:11:17','2025-05-21 15:11:22'),(316,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','thermotherapy','back',471,'2025-05-21 15:04:04','2025-05-21 15:11:55','2025-05-21 15:11:55'),(317,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','back',240,'2025-05-21 15:10:36','2025-05-21 15:14:36','2025-05-21 15:14:22'),(318,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','spheres','leg',798,'2025-05-21 15:00:38','2025-05-21 15:13:56','2025-05-21 15:14:53'),(319,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','finger','shoulder',931,'2025-05-21 15:03:33','2025-05-21 15:19:04','2025-05-21 15:18:50'),(320,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','back',928,'2025-05-21 15:04:37','2025-05-21 15:20:05','2025-05-21 15:20:04'),(321,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',71847,'2025-05-20 19:22:58','2025-05-21 15:20:25','2025-05-21 15:20:25'),(322,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','finger','shoulder',450,'2025-05-21 15:13:10','2025-05-21 15:20:40','2025-05-21 15:20:40'),(323,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',13,'2025-05-21 15:21:10','2025-05-21 15:21:23','2025-05-21 15:21:24'),(324,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',16,'2025-05-21 15:23:47','2025-05-21 15:24:03','2025-05-21 15:24:03'),(325,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',1850,'2025-05-21 14:54:28','2025-05-21 15:25:18','2025-05-21 15:25:17'),(326,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','roller','back',16,'2025-05-21 15:25:03','2025-05-21 15:25:19','2025-05-21 15:25:19'),(327,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','spheres','back',27,'2025-05-21 15:24:25','2025-05-21 15:24:52','2025-05-21 15:25:49'),(328,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','shockwave','back',38,'2025-05-21 15:27:02','2025-05-21 15:27:40','2025-05-21 15:27:38'),(329,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',947,'2025-05-21 15:12:57','2025-05-21 15:28:44','2025-05-21 15:28:49'),(330,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','spheres','leg',131,'2025-05-21 15:26:14','2025-05-21 15:28:25','2025-05-21 15:29:21'),(331,'673602f0d14760402fbd033b_jsfb-2C0A3E','jsfb-2C0A3E','RS-LL-X1','roller','leg',464,'2025-05-21 15:24:37','2025-05-21 15:32:21','2025-05-21 15:32:21'),(332,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','shockwave','back',37,'2025-05-21 15:32:18','2025-05-21 15:32:55','2025-05-21 15:32:54'),(333,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','spheres','leg',35,'2025-05-21 15:32:14','2025-05-21 15:32:49','2025-05-21 15:33:46'),(334,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','leg',17,'2025-05-21 15:36:19','2025-05-21 15:36:36','2025-05-21 15:36:36'),(335,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',19,'2025-05-21 15:36:25','2025-05-21 15:36:44','2025-05-21 15:36:39'),(336,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','spheres','back',108,'2025-05-21 15:34:50','2025-05-21 15:36:38','2025-05-21 15:37:35'),(337,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','back',953,'2025-05-21 15:23:00','2025-05-21 15:38:53','2025-05-21 15:38:52'),(338,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',20,'2025-05-21 15:40:07','2025-05-21 15:40:27','2025-05-21 15:40:21'),(339,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','thermotherapy','belly',2743,'2025-05-21 14:56:05','2025-05-21 15:41:48','2025-05-21 15:41:48'),(340,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',1060,'2025-05-21 15:24:56','2025-05-21 15:42:36','2025-05-21 15:42:22'),(341,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',19,'2025-05-21 15:45:02','2025-05-21 15:45:21','2025-05-21 15:45:17'),(342,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','stone','belly',938,'2025-05-21 15:32:25','2025-05-21 15:48:03','2025-05-21 15:48:07'),(343,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','spheres','leg',712,'2025-05-21 15:39:30','2025-05-21 15:51:22','2025-05-21 15:52:19'),(344,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',795,'2025-05-21 15:44:52','2025-05-21 15:58:07','2025-05-21 15:57:53'),(345,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',19,'2025-05-21 16:00:00','2025-05-21 16:00:19','2025-05-21 16:00:15'),(346,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',31,'2025-05-21 15:59:54','2025-05-21 16:00:25','2025-05-21 16:00:23'),(347,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','spheres','leg',128,'2025-05-21 15:58:07','2025-05-21 16:00:15','2025-05-21 16:01:11'),(348,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',20,'2025-05-21 16:02:47','2025-05-21 16:03:07','2025-05-21 16:03:02'),(349,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','spheres','leg',15,'2025-05-21 16:05:38','2025-05-21 16:05:53','2025-05-21 16:05:39'),(350,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',1830,'2025-05-21 15:43:11','2025-05-21 16:13:41','2025-05-21 16:13:40'),(351,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','thermotherapy','belly',1860,'2025-05-21 15:43:32','2025-05-21 16:14:32','2025-05-21 16:14:33'),(352,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','waist',935,'2025-05-21 16:00:27','2025-05-21 16:16:02','2025-05-21 16:16:06'),(353,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',927,'2025-05-21 16:01:08','2025-05-21 16:16:35','2025-05-21 16:16:33'),(354,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','roller','back',925,'2025-05-21 16:03:42','2025-05-21 16:19:07','2025-05-21 16:18:54'),(355,'673602f0d14760402fbd033b_jsfb-2C023E','jsfb-2C023E','RS-LL-X1','finger','back',15,'2025-05-21 16:20:15','2025-05-21 16:20:30','2025-05-21 16:20:30'),(356,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',1513,'2025-05-21 15:59:22','2025-05-21 16:24:35','2025-05-21 16:24:22'),(357,'673602f0d14760402fbd033b_jsfb-2C023E','jsfb-2C023E','RS-LL-X1','finger','back',364,'2025-05-21 16:21:14','2025-05-21 16:27:18','2025-05-21 16:27:18'),(358,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','back',352,'2025-05-21 16:27:55','2025-05-21 16:33:47','2025-05-21 16:33:33'),(359,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','waist',920,'2025-05-21 16:18:48','2025-05-21 16:34:08','2025-05-21 16:34:12'),(360,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','belly',138,'2025-05-21 16:39:46','2025-05-21 16:42:04','2025-05-21 16:41:50'),(361,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','thermotherapy','belly',1867,'2025-05-21 16:16:32','2025-05-21 16:47:39','2025-05-21 16:47:39'),(362,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','belly',432,'2025-05-21 16:43:28','2025-05-21 16:50:40','2025-05-21 16:50:25'),(363,'673602f0d14760402fbd033b_jsfb-2C023E','jsfb-2C023E','RS-LL-X1','shockwave','back',74,'2025-05-21 16:49:25','2025-05-21 16:50:39','2025-05-21 16:50:40'),(364,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','roller','back',20,'2025-05-21 16:55:09','2025-05-21 16:55:29','2025-05-21 16:55:25'),(365,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',1855,'2025-05-21 16:27:19','2025-05-21 16:58:14','2025-05-21 16:58:00'),(366,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','stone','back',328,'2025-05-21 16:53:32','2025-05-21 16:59:00','2025-05-21 16:58:58'),(367,'673602f0d14760402fbd033b_jsfb-2C023E','jsfb-2C023E','RS-LL-X1','shockwave','back',528,'2025-05-21 16:52:19','2025-05-21 17:01:07','2025-05-21 17:01:07'),(368,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','roller','back',20,'2025-05-21 17:14:43','2025-05-21 17:15:03','2025-05-21 17:14:58'),(369,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','thermotherapy','back',748,'2025-05-21 17:10:53','2025-05-21 17:23:21','2025-05-21 17:23:25'),(370,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','belly',925,'2025-05-21 17:09:09','2025-05-21 17:24:34','2025-05-21 17:24:19'),(371,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',1332,'2025-05-21 17:03:55','2025-05-21 17:26:07','2025-05-21 17:25:53'),(372,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','ball','back',20,'2025-05-21 17:32:51','2025-05-21 17:33:11','2025-05-21 17:33:06'),(373,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','belly',949,'2025-05-21 17:25:49','2025-05-21 17:41:38','2025-05-21 17:41:23'),(374,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',923,'2025-05-21 17:26:01','2025-05-21 17:41:24','2025-05-21 17:41:28'),(375,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',1023,'2025-05-21 17:29:02','2025-05-21 17:46:05','2025-05-21 17:45:51'),(376,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','spheres','leg',314,'2025-05-21 17:40:47','2025-05-21 17:46:01','2025-05-21 17:46:59'),(377,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','roller','back',21,'2025-05-21 17:54:47','2025-05-21 17:55:08','2025-05-21 17:55:03'),(378,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',20,'2025-05-21 17:56:12','2025-05-21 17:56:32','2025-05-21 17:56:27'),(379,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','belly',956,'2025-05-21 17:42:44','2025-05-21 17:58:40','2025-05-21 17:58:25'),(380,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','stone','back',948,'2025-05-21 17:45:46','2025-05-21 18:01:34','2025-05-21 18:01:39'),(381,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','shoulder',800,'2025-05-21 17:50:04','2025-05-21 18:03:24','2025-05-21 18:03:22'),(382,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','shoulder',493,'2025-05-21 18:09:35','2025-05-21 18:17:48','2025-05-21 18:17:47'),(383,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','shoulder',381,'2025-05-21 18:20:55','2025-05-21 18:27:16','2025-05-21 18:27:14'),(384,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','stone','belly',957,'2025-05-21 18:11:49','2025-05-21 18:27:46','2025-05-21 18:27:51'),(385,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','waist',31,'2025-05-21 18:46:02','2025-05-21 18:46:33','2025-05-21 18:46:32'),(386,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','stone','belly',932,'2025-05-21 18:40:23','2025-05-21 18:55:55','2025-05-21 18:56:00'),(387,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','thermotherapy','back',945,'2025-05-21 18:48:19','2025-05-21 19:04:04','2025-05-21 19:03:50'),(388,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','stone','belly',741,'2025-05-21 18:58:14','2025-05-21 19:10:35','2025-05-21 19:10:40'),(389,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','stone','back',749,'2025-05-21 19:22:31','2025-05-21 19:35:00','2025-05-21 19:35:05'),(390,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','thermotherapy','back',928,'2025-05-21 19:27:00','2025-05-21 19:42:28','2025-05-21 19:42:15'),(391,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',2721,'2025-05-21 19:10:25','2025-05-21 19:55:46','2025-05-21 19:55:45'),(392,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',946,'2025-05-21 19:44:41','2025-05-21 20:00:27','2025-05-21 20:00:31'),(393,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','shoulder',928,'2025-05-21 19:47:55','2025-05-21 20:03:23','2025-05-21 20:03:23'),(394,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',7224,'2025-05-21 18:05:37','2025-05-21 20:06:01','2025-05-21 20:05:59'),(395,'673602f0d14760402fbd033b_jsfb-llp','jsfb-llp','RS-LL-X1','finger','back',928,'2025-05-21 19:52:32','2025-05-21 20:08:00','2025-05-21 20:08:00'),(396,'673602f0d14760402fbd033b_jsfb-6E04FA','jsfb-6E04FA','RS-LL-X1','finger','back',29,'2025-05-21 20:14:45','2025-05-21 20:15:14','2025-05-21 20:15:14'),(397,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',951,'2025-05-21 20:02:12','2025-05-21 20:18:03','2025-05-21 20:18:07'),(398,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','thermotherapy','belly',966,'2025-05-21 20:02:16','2025-05-21 20:18:22','2025-05-21 20:18:08'),(399,'673602f0d14760402fbd033b_jsfb-6E04FA','jsfb-6E04FA','RS-LL-X1','finger','back',29,'2025-05-21 20:19:48','2025-05-21 20:20:17','2025-05-21 20:20:17'),(400,'673602f0d14760402fbd033b_jsfb-6E04FA','jsfb-6E04FA','RS-LL-X1','finger','back',43,'2025-05-21 20:23:05','2025-05-21 20:23:48','2025-05-21 20:23:48'),(401,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','stone','belly',931,'2025-05-21 20:09:21','2025-05-21 20:24:52','2025-05-21 20:24:53'),(402,'673602f0d14760402fbd033b_jsfb-6E04FA','jsfb-6E04FA','RS-LL-X1','finger','back',57,'2025-05-21 20:26:02','2025-05-21 20:26:59','2025-05-21 20:26:59'),(403,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','belly',2728,'2025-05-21 21:39:08','2025-05-21 22:24:36','2025-05-21 22:24:35'),(404,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','waist',1291,'2025-05-21 22:18:40','2025-05-21 22:40:11','2025-05-21 22:40:09'),(405,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','finger','back',940,'2025-05-21 22:57:16','2025-05-21 23:12:56','2025-05-21 23:12:55'),(406,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','finger','waist',935,'2025-05-21 23:14:25','2025-05-21 23:30:00','2025-05-21 23:29:58'),(407,'673602f0d14760402fbd033b_jsfb-6E04FA','jsfb-6E04FA','RS-LL-X1','finger','back',245,'2025-05-22 07:54:09','2025-05-22 07:58:14','2025-05-22 07:58:14'),(408,'673602f0d14760402fbd033b_jsfb-6E04FA','jsfb-6E04FA','RS-LL-X1','finger','back',924,'2025-05-22 08:00:26','2025-05-22 08:15:50','2025-05-22 08:15:50'),(409,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',65,'2025-05-22 08:31:11','2025-05-22 08:32:16','2025-05-22 08:32:16'),(410,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','stone','back',54,'2025-05-22 08:42:45','2025-05-22 08:43:39','2025-05-22 08:43:39'),(411,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','stone','back',127,'2025-05-22 08:56:25','2025-05-22 08:58:32','2025-05-22 08:58:32'),(412,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',116,'2025-05-22 09:01:02','2025-05-22 09:02:58','2025-05-22 09:02:58'),(413,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',180,'2025-05-22 09:06:05','2025-05-22 09:09:05','2025-05-22 09:09:05'),(414,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',103,'2025-05-22 09:10:19','2025-05-22 09:12:02','2025-05-22 09:12:02'),(415,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',70,'2025-05-22 09:16:31','2025-05-22 09:17:41','2025-05-22 09:17:41'),(416,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',20,'2025-05-22 09:18:05','2025-05-22 09:18:25','2025-05-22 09:18:20'),(417,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',56,'2025-05-22 09:24:52','2025-05-22 09:25:48','2025-05-22 09:25:48'),(418,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',20,'2025-05-22 09:28:08','2025-05-22 09:28:28','2025-05-22 09:28:28'),(419,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',929,'2025-05-22 09:13:40','2025-05-22 09:29:09','2025-05-22 09:29:13'),(420,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',75,'2025-05-22 09:30:14','2025-05-22 09:31:29','2025-05-22 09:31:29'),(421,'673602f0d14760402fbd033b_jsfb-DE34AD','jsfb-DE34AD','RS-LL-X1','finger','back',1852,'2025-05-22 09:06:08','2025-05-22 09:37:00','2025-05-22 09:37:00'),(422,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',20,'2025-05-22 09:36:47','2025-05-22 09:37:07','2025-05-22 09:37:07'),(423,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',20,'2025-05-22 09:38:46','2025-05-22 09:39:06','2025-05-22 09:39:07'),(424,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',643,'2025-05-22 09:33:23','2025-05-22 09:44:06','2025-05-22 09:44:06'),(425,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','belly',21,'2025-05-22 09:45:45','2025-05-22 09:46:06','2025-05-22 09:45:51'),(426,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',20,'2025-05-22 09:45:57','2025-05-22 09:46:17','2025-05-22 09:46:18'),(427,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',1722,'2025-05-22 09:19:31','2025-05-22 09:48:13','2025-05-22 09:48:00'),(428,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',20,'2025-05-22 09:47:52','2025-05-22 09:48:12','2025-05-22 09:48:13'),(429,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','roller','back',20,'2025-05-22 09:48:56','2025-05-22 09:49:16','2025-05-22 09:49:16'),(430,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','roller','back',20,'2025-05-22 09:51:06','2025-05-22 09:51:26','2025-05-22 09:51:27'),(431,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','stone','belly',956,'2025-05-22 09:37:45','2025-05-22 09:53:41','2025-05-22 09:53:46'),(432,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','stone','back',1156,'2025-05-22 09:37:35','2025-05-22 09:56:51','2025-05-22 09:56:50'),(433,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',462,'2025-05-22 09:54:11','2025-05-22 10:01:53','2025-05-22 10:01:51'),(434,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','belly',36,'2025-05-22 10:01:45','2025-05-22 10:02:21','2025-05-22 10:02:06'),(435,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',794,'2025-05-22 09:49:51','2025-05-22 10:03:05','2025-05-22 10:03:06'),(436,'673602f0d14760402fbd033b_jsfb-DE34AD','jsfb-DE34AD','RS-LL-X1','stone','back',928,'2025-05-22 09:49:25','2025-05-22 10:04:53','2025-05-22 10:04:54'),(437,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',51,'2025-05-22 10:04:14','2025-05-22 10:05:05','2025-05-22 10:05:06'),(438,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','roller','back',21,'2025-05-22 10:05:54','2025-05-22 10:06:15','2025-05-22 10:06:15'),(439,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',96,'2025-05-22 10:06:05','2025-05-22 10:07:41','2025-05-22 10:07:41'),(440,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',1057,'2025-05-22 09:50:20','2025-05-22 10:07:57','2025-05-22 10:07:44'),(441,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','belly',363,'2025-05-22 10:03:42','2025-05-22 10:09:45','2025-05-22 10:09:29'),(442,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',272,'2025-05-22 10:08:32','2025-05-22 10:13:04','2025-05-22 10:13:04'),(443,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','back',947,'2025-05-22 09:57:55','2025-05-22 10:13:42','2025-05-22 10:13:46'),(444,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','finger','back',938,'2025-05-22 09:58:34','2025-05-22 10:14:12','2025-05-22 10:14:11'),(445,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',442,'2025-05-22 10:14:27','2025-05-22 10:21:49','2025-05-22 10:21:49'),(446,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',924,'2025-05-22 10:10:41','2025-05-22 10:26:05','2025-05-22 10:25:52'),(447,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',189,'2025-05-22 10:22:52','2025-05-22 10:26:01','2025-05-22 10:26:01'),(448,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','heat','belly',210,'2025-05-22 10:26:17','2025-05-22 10:29:47','2025-05-22 10:29:32'),(449,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','roller','back',935,'2025-05-22 10:15:34','2025-05-22 10:31:09','2025-05-22 10:31:07'),(450,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',946,'2025-05-22 10:17:14','2025-05-22 10:33:00','2025-05-22 10:33:05'),(451,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','stone','belly',1846,'2025-05-22 10:05:48','2025-05-22 10:36:34','2025-05-22 10:36:34'),(452,'673602f0d14760402fbd033b_jsfb-DE34AD','jsfb-DE34AD','RS-LL-X1','stone','back',1831,'2025-05-22 10:08:05','2025-05-22 10:38:36','2025-05-22 10:38:37'),(453,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',919,'2025-05-22 10:28:41','2025-05-22 10:44:00','2025-05-22 10:44:01'),(454,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',25,'2025-05-22 10:45:12','2025-05-22 10:45:37','2025-05-22 10:45:38'),(455,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',924,'2025-05-22 10:32:01','2025-05-22 10:47:25','2025-05-22 10:47:12'),(456,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',949,'2025-05-22 10:34:17','2025-05-22 10:50:06','2025-05-22 10:50:10'),(457,'673602f0d14760402fbd033b_jsfb-DE34AD','jsfb-DE34AD','RS-LL-X1','stone','belly',685,'2025-05-22 10:41:55','2025-05-22 10:53:20','2025-05-22 10:53:20'),(458,'673602f0d14760402fbd033b_jsfb-960460','jsfb-960460','RS-LL-X1','ball','back',20,'2025-05-22 10:55:27','2025-05-22 10:55:47','2025-05-22 10:55:46'),(459,'673602f0d14760402fbd033b_jsfb-960460','jsfb-960460','RS-LL-X1','ball','back',20,'2025-05-22 10:59:35','2025-05-22 10:59:55','2025-05-22 10:59:55'),(460,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',795,'2025-05-22 10:46:56','2025-05-22 11:00:11','2025-05-22 11:00:12'),(461,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',118,'2025-05-22 11:01:23','2025-05-22 11:03:21','2025-05-22 11:03:21'),(462,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',954,'2025-05-22 10:49:09','2025-05-22 11:05:03','2025-05-22 11:04:50'),(463,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',944,'2025-05-22 10:55:19','2025-05-22 11:11:03','2025-05-22 11:11:08'),(464,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',447,'2025-05-22 11:05:12','2025-05-22 11:12:39','2025-05-22 11:12:39'),(465,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',89,'2025-05-22 11:13:17','2025-05-22 11:14:46','2025-05-22 11:14:50'),(466,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',142,'2025-05-22 11:13:59','2025-05-22 11:16:21','2025-05-22 11:16:22'),(467,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',19,'2025-05-22 11:17:50','2025-05-22 11:18:09','2025-05-22 11:18:10'),(468,'673602f0d14760402fbd033b_jsfb-DE34AD','jsfb-DE34AD','RS-LL-X1','shockwave','back',1822,'2025-05-22 10:55:35','2025-05-22 11:25:57','2025-05-22 11:25:57'),(469,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',950,'2025-05-22 11:13:28','2025-05-22 11:29:18','2025-05-22 11:29:05'),(470,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',926,'2025-05-22 11:15:40','2025-05-22 11:31:06','2025-05-22 11:31:11'),(471,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',910,'2025-05-22 11:20:28','2025-05-22 11:35:38','2025-05-22 11:35:44'),(472,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',20,'2025-05-22 11:37:10','2025-05-22 11:37:30','2025-05-22 11:37:30'),(473,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','roller','back',20,'2025-05-22 11:41:44','2025-05-22 11:42:04','2025-05-22 11:42:04'),(474,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','back',934,'2025-05-22 11:32:05','2025-05-22 11:47:39','2025-05-22 11:47:38'),(475,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',919,'2025-05-22 11:32:36','2025-05-22 11:47:55','2025-05-22 11:47:59'),(476,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',952,'2025-05-22 11:38:33','2025-05-22 11:54:25','2025-05-22 11:54:12'),(477,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',936,'2025-05-22 11:39:12','2025-05-22 11:54:48','2025-05-22 11:54:49'),(478,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',16,'2025-05-22 11:56:00','2025-05-22 11:56:16','2025-05-22 11:56:16'),(479,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','finger','back',407,'2025-05-22 11:49:52','2025-05-22 11:56:39','2025-05-22 11:56:38'),(480,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','roller','back',1822,'2025-05-22 11:27:15','2025-05-22 11:57:37','2025-05-22 11:57:36'),(481,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','finger','back',60,'2025-05-22 12:03:47','2025-05-22 12:04:47','2025-05-22 12:04:47'),(482,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','back',1687,'2025-05-22 11:37:22','2025-05-22 12:05:29','2025-05-22 12:05:29'),(483,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',946,'2025-05-22 11:50:43','2025-05-22 12:06:29','2025-05-22 12:06:33'),(484,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',933,'2025-05-22 11:56:59','2025-05-22 12:12:32','2025-05-22 12:12:19'),(485,'673602f0d14760402fbd033b_jsfb-E20A6A','jsfb-E20A6A','RS-LL-X1','roller','waist',934,'2025-05-22 11:58:38','2025-05-22 12:14:12','2025-05-22 12:14:11'),(486,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','shockwave','back',802,'2025-05-22 12:07:04','2025-05-22 12:20:26','2025-05-22 12:20:25'),(487,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','finger','back',683,'2025-05-22 12:09:27','2025-05-22 12:20:50','2025-05-22 12:20:50'),(488,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',922,'2025-05-22 12:08:31','2025-05-22 12:23:53','2025-05-22 12:23:57'),(489,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',1681,'2025-05-22 11:57:29','2025-05-22 12:25:30','2025-05-22 12:25:31'),(490,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',801,'2025-05-22 12:16:53','2025-05-22 12:30:14','2025-05-22 12:30:01'),(491,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',254,'2025-05-22 12:26:42','2025-05-22 12:30:56','2025-05-22 12:30:56'),(492,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','shockwave','back',486,'2025-05-22 12:27:41','2025-05-22 12:35:47','2025-05-22 12:35:47'),(493,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','thermotherapy','back',938,'2025-05-22 12:25:10','2025-05-22 12:40:48','2025-05-22 12:40:47'),(494,'673602f0d14760402fbd033b_jsfb-75925E','jsfb-75925E','RS-LL-X1','finger','shoulder',1860,'2025-05-22 12:09:57','2025-05-22 12:40:57','2025-05-22 12:40:57'),(495,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','thermotherapy','belly',971,'2025-05-22 12:28:43','2025-05-22 12:44:54','2025-05-22 12:44:58'),(496,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',824,'2025-05-22 12:32:02','2025-05-22 12:45:46','2025-05-22 12:45:33'),(497,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',922,'2025-05-22 12:34:51','2025-05-22 12:50:13','2025-05-22 12:50:13'),(498,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',931,'2025-05-22 12:49:52','2025-05-22 13:05:23','2025-05-22 13:05:10'),(499,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',923,'2025-05-22 12:51:37','2025-05-22 13:07:00','2025-05-22 13:07:01'),(500,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',942,'2025-05-22 13:07:56','2025-05-22 13:23:38','2025-05-22 13:23:25'),(501,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','finger','back',441,'2025-05-22 13:22:10','2025-05-22 13:29:31','2025-05-22 13:29:15'),(502,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',20,'2025-05-22 13:36:22','2025-05-22 13:36:42','2025-05-22 13:36:40'),(503,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','stone','back',370,'2025-05-22 13:31:39','2025-05-22 13:37:49','2025-05-22 13:37:33'),(504,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','spheres','leg',88,'2025-05-22 13:35:13','2025-05-22 13:36:41','2025-05-22 13:37:40'),(505,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',937,'2025-05-22 13:27:20','2025-05-22 13:42:57','2025-05-22 13:42:44'),(506,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',251,'2025-05-22 13:44:57','2025-05-22 13:49:08','2025-05-22 13:49:08'),(507,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',20,'2025-05-22 13:52:18','2025-05-22 13:52:38','2025-05-22 13:52:36'),(508,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','stone','back',341,'2025-05-22 13:48:02','2025-05-22 13:53:43','2025-05-22 13:53:27'),(509,'673602f0d14760402fbd033b_jsfb-6EAC57','jsfb-6EAC57','RS-LL-X1','spheres','leg',927,'2025-05-22 13:43:45','2025-05-22 13:59:12','2025-05-22 14:00:11'),(510,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',20,'2025-05-22 14:02:12','2025-05-22 14:02:32','2025-05-22 14:02:30'),(511,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','shoulder',954,'2025-05-22 13:59:02','2025-05-22 14:14:56','2025-05-22 14:15:00'),(512,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','stone','belly',49,'2025-05-22 14:19:38','2025-05-22 14:20:27','2025-05-22 14:20:11'),(513,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',925,'2025-05-22 14:06:00','2025-05-22 14:21:25','2025-05-22 14:21:25'),(514,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',934,'2025-05-22 14:08:03','2025-05-22 14:23:37','2025-05-22 14:23:24'),(515,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','shockwave','back',1819,'2025-05-22 14:00:29','2025-05-22 14:30:48','2025-05-22 14:30:34'),(516,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','thermotherapy','back',133,'2025-05-22 14:32:35','2025-05-22 14:34:48','2025-05-22 14:34:35'),(517,'673602f0d14760402fbd033b_jsfb-CAC156','jsfb-CAC156','RS-LL-X1','roller','back',26,'2025-05-22 14:35:04','2025-05-22 14:35:30','2025-05-22 14:35:28'),(518,'673602f0d14760402fbd033b_jsfb-D322F9','jsfb-D322F9','RS-LL-X1','finger','back',19,'2025-05-22 14:38:50','2025-05-22 14:39:09','2025-05-22 14:39:09'),(519,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',953,'2025-05-22 14:26:19','2025-05-22 14:42:12','2025-05-22 14:41:59'),(520,'673602f0d14760402fbd033b_jsfb-EC7376','jsfb-EC7376','RS-LL-X1','roller','leg',766,'2025-05-22 14:41:28','2025-05-22 14:54:14','2025-05-22 14:54:12'),(521,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','finger','waist',343,'2025-05-22 14:49:37','2025-05-22 14:55:20','2025-05-22 14:55:20'),(522,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',938,'2025-05-22 14:43:15','2025-05-22 14:58:53','2025-05-22 14:58:40'),(523,'673602f0d14760402fbd033b_jsfb-EC37A5','jsfb-EC37A5','RS-LL-X1','thermotherapy','back',1678,'2025-05-22 14:36:31','2025-05-22 15:04:29','2025-05-22 15:04:16'),(524,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','thermotherapy','back',957,'2025-05-22 14:49:49','2025-05-22 15:05:46','2025-05-22 15:05:45'),(525,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','finger','shoulder',80,'2025-05-22 15:08:26','2025-05-22 15:09:46','2025-05-22 15:09:30'),(526,'673602f0d14760402fbd033b_jsfb-realman2','jsfb-realman2','RS-LL-X1','ball','back',7230,'2025-05-22 13:09:48','2025-05-22 15:10:18','2025-05-22 15:10:19'),(527,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','finger','back',825,'2025-05-22 15:01:33','2025-05-22 15:15:18','2025-05-22 15:15:18'),(528,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','finger','shoulder',891,'2025-05-22 15:10:44','2025-05-22 15:25:35','2025-05-22 15:25:19'),(529,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','finger','waist',332,'2025-05-22 15:21:49','2025-05-22 15:27:21','2025-05-22 15:27:21'),(530,'673602f0d14760402fbd033b_jsfb-FD6E46','jsfb-FD6E46','RS-LL-X1','stone','belly',1573,'2025-05-22 15:01:33','2025-05-22 15:27:46','2025-05-22 15:27:33'),(531,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','stone','back',922,'2025-05-22 15:14:47','2025-05-22 15:30:09','2025-05-22 15:30:08'),(532,'673602f0d14760402fbd033b_jsfb-0846B3','jsfb-0846B3','RS-LL-X1','shockwave','shoulder',947,'2025-05-22 15:29:16','2025-05-22 15:45:03','2025-05-22 15:44:47'),(533,'673602f0d14760402fbd033b_jsfb-B76DFB','jsfb-B76DFB','RS-LL-X1','finger','waist',923,'2025-05-22 15:30:18','2025-05-22 15:45:41','2025-05-22 15:45:41'),(534,'673602f0d14760402fbd033b_jsfb-340FCB','jsfb-340FCB','RS-LL-X1','finger','back',942,'2025-05-22 15:30:11','2025-05-22 15:45:53','2025-05-22 15:45:57'),(535,'673602f0d14760402fbd033b_jsfb-D9CC3E','jsfb-D9CC3E','RS-LL-X1','roller','back',919,'2025-05-22 15:32:52','2025-05-22 15:48:11','2025-05-22 15:48:10'); +/*!40000 ALTER TABLE `massage_task` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping routines for database 'storm-device' +-- +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-03 21:30:37 diff --git a/docker/mysql/db/dump-storm-fixed.sql b/docker/mysql/db/dump-storm-fixed.sql new file mode 100644 index 0000000..baeb424 --- /dev/null +++ b/docker/mysql/db/dump-storm-fixed.sql @@ -0,0 +1,87 @@ +-- MySQL dump 10.13 Distrib 8.0.42, for Win64 (x86_64) +-- +-- Host: 192.168.48.128 Database: storm +-- ------------------------------------------------------ +-- Server version 5.7.44 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `device` +-- + +DROP DATABASE IF EXISTS `storm`; + +CREATE DATABASE `storm` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +USE `storm`; + +DROP TABLE IF EXISTS `device`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `device` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `device_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备唯一标识', + `device_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备名称', + `remark` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', + `product_id` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品ID', + `product_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品名称', + `device_type` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备类型', + `location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '安装位置', + `shop_id` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '店铺ID', + `longitude` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '经度', + `latitude` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '纬度', + `software_version` varchar(15) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '软件版本', + `vtxdb_version` varchar(15) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '参数服务器版本', + `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备描述', + `is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建人ID', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新人ID', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_device_id` (`device_id`), + UNIQUE KEY `uk_device_name` (`device_name`), + KEY `idx_product_id` (`product_id`), + KEY `idx_device_type` (`device_type`), + KEY `idx_shop_id` (`shop_id`), + KEY `idx_location` (`location`(50)), + KEY `idx_is_deleted` (`is_deleted`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备基本信息表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `device` +-- + +LOCK TABLES `device` WRITE; +/*!40000 ALTER TABLE `device` DISABLE KEYS */; +/*!40000 ALTER TABLE `device` ENABLE KEYS */; +UNLOCK TABLES; + +SET FOREIGN_KEY_CHECKS = 1; + +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-09-03 21:28:28 diff --git a/docker/mysql/db/readme.txt b/docker/mysql/db/readme.txt new file mode 100644 index 0000000..0b22f3f --- /dev/null +++ b/docker/mysql/db/readme.txt @@ -0,0 +1 @@ +sqlĿ¼µнűdockerԶִС \ No newline at end of file diff --git a/docker/mysql/dockerfile b/docker/mysql/dockerfile new file mode 100644 index 0000000..91c40f3 --- /dev/null +++ b/docker/mysql/dockerfile @@ -0,0 +1,5 @@ +# 基础镜像 +FROM mysql:5.7 + +# 执行sql脚本 +ADD ./db/*.sql /docker-entrypoint-initdb.d/ diff --git a/docker/nacos/conf/application.properties b/docker/nacos/conf/application.properties new file mode 100644 index 0000000..4e8bf54 --- /dev/null +++ b/docker/nacos/conf/application.properties @@ -0,0 +1,33 @@ +spring.datasource.platform=mysql +db.num=1 +db.url.0=jdbc:mysql://storm-mysql:3306/storm-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC +db.user=root +db.password=123456 + +nacos.naming.empty-service.auto-clean=true +nacos.naming.empty-service.clean.initial-delay-ms=50000 +nacos.naming.empty-service.clean.period-time-ms=30000 + +management.endpoints.web.exposure.include=* + +management.metrics.export.elastic.enabled=false +management.metrics.export.influx.enabled=false + +server.tomcat.accesslog.enabled=true +server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D %{User-Agent}i %{Request-Source}i + +server.tomcat.basedir=/home/storm/nacos/tomcat/logs + +nacos.security.ignore.urls=/,/error,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-ui/public/**,/v1/auth/**,/v1/console/health/**,/actuator/**,/v1/console/server/** + +nacos.core.auth.system.type=nacos +nacos.core.auth.enabled=true +nacos.core.auth.plugin.nacos.token.expire.seconds=18000 +nacos.core.auth.plugin.nacos.enable=true +nacos.core.auth.plugin.nacos.token.secret.key=wczCOGn5ryZtpt+MdZxv+XxmrER3sYnhE4Mefto8lmc= +nacos.core.auth.caching.enabled=true +nacos.core.auth.enable.userAgentAuthWhite=false +nacos.core.auth.server.identity.key=serverIdentity +nacos.core.auth.server.identity.value=security + +nacos.istio.mcp.server.enabled=false diff --git a/docker/nacos/dockerfile b/docker/nacos/dockerfile new file mode 100644 index 0000000..7baae86 --- /dev/null +++ b/docker/nacos/dockerfile @@ -0,0 +1,5 @@ +# 基础镜像 +FROM nacos/nacos-server:v2.3.0 + +# 复制conf文件到路径 +COPY ./conf/application.properties /home/nacos/conf/application.properties diff --git a/docker/nginx/conf/nginx.conf b/docker/nginx/conf/nginx.conf new file mode 100644 index 0000000..20c96ff --- /dev/null +++ b/docker/nginx/conf/nginx.conf @@ -0,0 +1,41 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + + server { + listen 80; + server_name localhost; + + location / { + root /home/storm/projects/storm-ui; + try_files $uri $uri/ /index.html; + index index.html index.htm; + } + + location /prod-api/{ + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://storm-gateway:8080/; + } + + # 避免actuator暴露 + if ($uri ~ "/actuator") { + return 403; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + } +} \ No newline at end of file diff --git a/docker/nginx/dockerfile b/docker/nginx/dockerfile new file mode 100644 index 0000000..84b0461 --- /dev/null +++ b/docker/nginx/dockerfile @@ -0,0 +1,13 @@ +# 基础镜像 +FROM nginx + +# 挂载目录 +VOLUME /home/storm/projects/storm-ui +# 创建目录 +RUN mkdir -p /home/storm/projects/storm-ui +# 指定路径 +WORKDIR /home/storm/projects/storm-ui +# 复制conf文件到路径 +COPY ./conf/nginx.conf /etc/nginx/nginx.conf +# 复制html文件到路径 +COPY ./html/dist /home/storm/projects/storm-ui diff --git a/docker/redis/conf/redis.conf b/docker/redis/conf/redis.conf new file mode 100644 index 0000000..366ee23 --- /dev/null +++ b/docker/redis/conf/redis.conf @@ -0,0 +1,10 @@ +# requirepass 123456 +appendonly yes +appendfilename "appendonly.aof" +dir /data +appendfsync everysec +maxmemory 2gb +maxmemory-policy allkeys-lru +save 60 10000 +stop-writes-on-bgsave-error no +bind 0.0.0.0 \ No newline at end of file diff --git a/docker/redis/dockerfile b/docker/redis/dockerfile new file mode 100644 index 0000000..80c8b19 --- /dev/null +++ b/docker/redis/dockerfile @@ -0,0 +1,9 @@ +# 基础镜像 +FROM redis + +# 创建目录 +RUN mkdir -p /usr/local/etc/redis +# 复制conf文件到路径 +COPY ./conf/redis.conf /usr/local/etc/redis/redis.conf +# 设置数据目录权限 +RUN chown -R redis:redis /data diff --git a/docker/storm/auth/dockerfile b/docker/storm/auth/dockerfile new file mode 100644 index 0000000..04d34e0 --- /dev/null +++ b/docker/storm/auth/dockerfile @@ -0,0 +1,15 @@ +# 基础镜像 +FROM openjdk:11 + +# 挂载目录 +VOLUME /home/storm +# 创建目录 +RUN mkdir -p /home/storm +# 指定路径 +WORKDIR /home/storm +# 复制jar文件到路径 +COPY ./jar/storm-auth.jar /home/storm/storm-auth.jar +# 设置 headless 模式 +ENV JAVA_OPTS="-Djava.awt.headless=true" +# 启动认证服务 +ENTRYPOINT ["java","-jar","storm-auth.jar"] \ No newline at end of file diff --git a/docker/storm/auth/jar/readme.txt b/docker/storm/auth/jar/readme.txt new file mode 100644 index 0000000..c35ba27 --- /dev/null +++ b/docker/storm/auth/jar/readme.txt @@ -0,0 +1 @@ +֤ĴõjarļdockerӦá \ No newline at end of file diff --git a/docker/storm/auth/jar/storm-auth.jar b/docker/storm/auth/jar/storm-auth.jar new file mode 100644 index 0000000..4affb95 Binary files /dev/null and b/docker/storm/auth/jar/storm-auth.jar differ diff --git a/docker/storm/gateway/dockerfile b/docker/storm/gateway/dockerfile new file mode 100644 index 0000000..662631c --- /dev/null +++ b/docker/storm/gateway/dockerfile @@ -0,0 +1,15 @@ +# 基础镜像 +FROM openjdk:11 + +# 挂载目录 +VOLUME /home/storm +# 创建目录 +RUN mkdir -p /home/storm +# 指定路径 +WORKDIR /home/storm +# 复制jar文件到路径 +COPY ./jar/storm-gateway.jar /home/storm/storm-gateway.jar +# 设置 headless 模式 +ENV JAVA_OPTS="-Djava.awt.headless=true" +# 启动网关服务 +ENTRYPOINT ["java","-jar","storm-gateway.jar"] \ No newline at end of file diff --git a/docker/storm/gateway/jar/readme.txt b/docker/storm/gateway/jar/readme.txt new file mode 100644 index 0000000..5dfbec7 --- /dev/null +++ b/docker/storm/gateway/jar/readme.txt @@ -0,0 +1 @@ +ģõjarļdockerӦá \ No newline at end of file diff --git a/docker/storm/gateway/jar/storm-gateway.jar b/docker/storm/gateway/jar/storm-gateway.jar new file mode 100644 index 0000000..305c8d4 Binary files /dev/null and b/docker/storm/gateway/jar/storm-gateway.jar differ diff --git a/docker/storm/modules/device/dockerfile b/docker/storm/modules/device/dockerfile new file mode 100644 index 0000000..8465a10 --- /dev/null +++ b/docker/storm/modules/device/dockerfile @@ -0,0 +1,20 @@ +# 基础镜像 +FROM openjdk:11-jre + +# 挂载目录 +VOLUME /home/storm + +# 创建目录 +RUN mkdir -p /home/storm + +# 指定路径 +WORKDIR /home/storm + +# 复制jar文件到路径 +COPY ./jar/storm-modules-device.jar /home/storm/storm-modules-device.jar + +# 设置 headless 模式 +ENV JAVA_OPTS="-Djava.awt.headless=true" + +# 启动设备服务 +ENTRYPOINT ["java","-jar","storm-modules-device.jar"] \ No newline at end of file diff --git a/docker/storm/modules/device/jar/storm-modules-device.jar b/docker/storm/modules/device/jar/storm-modules-device.jar new file mode 100644 index 0000000..18b2d30 Binary files /dev/null and b/docker/storm/modules/device/jar/storm-modules-device.jar differ diff --git a/docker/storm/modules/file/dockerfile b/docker/storm/modules/file/dockerfile new file mode 100644 index 0000000..54ecd78 --- /dev/null +++ b/docker/storm/modules/file/dockerfile @@ -0,0 +1,13 @@ +# 基础镜像 +FROM openjdk:11-jre + +# 挂载目录 +VOLUME /home/storm +# 创建目录 +RUN mkdir -p /home/storm +# 指定路径 +WORKDIR /home/storm +# 复制jar文件到路径 +COPY ./jar/storm-modules-file.jar /home/storm/storm-modules-file.jar +# 启动文件服务 +ENTRYPOINT ["java","-jar","storm-modules-file.jar"] \ No newline at end of file diff --git a/docker/storm/modules/file/jar/readme.txt b/docker/storm/modules/file/jar/readme.txt new file mode 100644 index 0000000..bf2b2a7 --- /dev/null +++ b/docker/storm/modules/file/jar/readme.txt @@ -0,0 +1 @@ +ļõjarļdockerӦá \ No newline at end of file diff --git a/docker/storm/modules/file/jar/storm-modules-file.jar b/docker/storm/modules/file/jar/storm-modules-file.jar new file mode 100644 index 0000000..a9fc33c Binary files /dev/null and b/docker/storm/modules/file/jar/storm-modules-file.jar differ diff --git a/docker/storm/modules/gen/dockerfile b/docker/storm/modules/gen/dockerfile new file mode 100644 index 0000000..37bc440 --- /dev/null +++ b/docker/storm/modules/gen/dockerfile @@ -0,0 +1,15 @@ +# 基础镜像 +FROM openjdk:11-jre + +# 挂载目录 +VOLUME /home/storm +# 创建目录 +RUN mkdir -p /home/storm +# 指定路径 +WORKDIR /home/storm +# 复制jar文件到路径 +COPY ./jar/storm-modules-gen.jar /home/storm/storm-modules-gen.jar +# 设置 headless 模式 +ENV JAVA_OPTS="-Djava.awt.headless=true" +# 启动代码生成服务 +ENTRYPOINT ["java","-jar","storm-modules-gen.jar"] \ No newline at end of file diff --git a/docker/storm/modules/gen/jar/readme.txt b/docker/storm/modules/gen/jar/readme.txt new file mode 100644 index 0000000..2f25c0a --- /dev/null +++ b/docker/storm/modules/gen/jar/readme.txt @@ -0,0 +1 @@ +ŴɴõjarļdockerӦá \ No newline at end of file diff --git a/docker/storm/modules/gen/jar/storm-modules-gen.jar b/docker/storm/modules/gen/jar/storm-modules-gen.jar new file mode 100644 index 0000000..f15c1b8 Binary files /dev/null and b/docker/storm/modules/gen/jar/storm-modules-gen.jar differ diff --git a/docker/storm/modules/job/dockerfile b/docker/storm/modules/job/dockerfile new file mode 100644 index 0000000..43a266b --- /dev/null +++ b/docker/storm/modules/job/dockerfile @@ -0,0 +1,13 @@ +# 基础镜像 +FROM openjdk:11-jre + +# 挂载目录 +VOLUME /home/storm +# 创建目录 +RUN mkdir -p /home/storm +# 指定路径 +WORKDIR /home/storm +# 复制jar文件到路径 +COPY ./jar/storm-modules-job.jar /home/storm/storm-modules-job.jar +# 启动定时任务服务 +ENTRYPOINT ["java","-jar","storm-modules-job.jar"] \ No newline at end of file diff --git a/docker/storm/modules/job/jar/readme.txt b/docker/storm/modules/job/jar/readme.txt new file mode 100644 index 0000000..58aea0b --- /dev/null +++ b/docker/storm/modules/job/jar/readme.txt @@ -0,0 +1 @@ +ŶʱõjarļdockerӦá \ No newline at end of file diff --git a/docker/storm/modules/job/jar/storm-modules-job.jar b/docker/storm/modules/job/jar/storm-modules-job.jar new file mode 100644 index 0000000..4a9f53b Binary files /dev/null and b/docker/storm/modules/job/jar/storm-modules-job.jar differ diff --git a/docker/storm/modules/system/dockerfile b/docker/storm/modules/system/dockerfile new file mode 100644 index 0000000..2f77df1 --- /dev/null +++ b/docker/storm/modules/system/dockerfile @@ -0,0 +1,15 @@ +# 基础镜像 +FROM openjdk:11-jre + +# 挂载目录 +VOLUME /home/storm +# 创建目录 +RUN mkdir -p /home/storm +# 指定路径 +WORKDIR /home/storm +# 复制jar文件到路径 +COPY ./jar/storm-modules-system.jar /home/storm/storm-modules-system.jar +# 设置 headless 模式 +ENV JAVA_OPTS="-Djava.awt.headless=true" +# 启动系统服务 +ENTRYPOINT ["java","-jar","storm-modules-system.jar"] \ No newline at end of file diff --git a/docker/storm/modules/system/jar/readme.txt b/docker/storm/modules/system/jar/readme.txt new file mode 100644 index 0000000..cfc2a92 --- /dev/null +++ b/docker/storm/modules/system/jar/readme.txt @@ -0,0 +1 @@ +ϵͳģõjarļdockerӦá \ No newline at end of file diff --git a/docker/storm/modules/system/jar/storm-modules-system.jar b/docker/storm/modules/system/jar/storm-modules-system.jar new file mode 100644 index 0000000..9e07ed4 Binary files /dev/null and b/docker/storm/modules/system/jar/storm-modules-system.jar differ diff --git a/docker/storm/visual/monitor/dockerfile b/docker/storm/visual/monitor/dockerfile new file mode 100644 index 0000000..e50d5ea --- /dev/null +++ b/docker/storm/visual/monitor/dockerfile @@ -0,0 +1,13 @@ +# 基础镜像 +FROM openjdk:11-jre + +# 挂载目录 +VOLUME /home/storm +# 创建目录 +RUN mkdir -p /home/storm +# 指定路径 +WORKDIR /home/storm +# 复制jar文件到路径 +COPY ./jar/storm-visual-monitor.jar /home/storm/storm-visual-monitor.jar +# 启动系统服务 +ENTRYPOINT ["java","-jar","storm-visual-monitor.jar"] \ No newline at end of file diff --git a/docker/storm/visual/monitor/jar/readme.txt b/docker/storm/visual/monitor/jar/readme.txt new file mode 100644 index 0000000..62b2841 --- /dev/null +++ b/docker/storm/visual/monitor/jar/readme.txt @@ -0,0 +1 @@ +żĴõjarļdockerӦá \ No newline at end of file diff --git a/docker/storm/visual/monitor/jar/storm-visual-monitor.jar b/docker/storm/visual/monitor/jar/storm-visual-monitor.jar new file mode 100644 index 0000000..62022f8 Binary files /dev/null and b/docker/storm/visual/monitor/jar/storm-visual-monitor.jar differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..924692c --- /dev/null +++ b/pom.xml @@ -0,0 +1,405 @@ + + + 4.0.0 + + com.storm + storm + 3.6.6 + + storm + http://www.storm.vip + 后台管理 + + + 3.6.6 + UTF-8 + UTF-8 + 11 + 2.7.18 + 2021.0.9 + 2021.0.6.1 + 2.7.16 + 1.27.2 + 2.3.3 + 2.0.0 + 1.2.23 + 4.3.1 + 2.19.0 + 2.3 + 2.0.57 + 0.9.1 + 8.2.2 + 4.1.2 + 4.3.0 + 2.14.4 + + 9.0.108 + 1.2.13 + 5.3.39 + 1.18.22 + 3.5.3 + 3.21.12 + 2.2.19 + 5.8.10 + 3.1.76 + 3.5.4 + 4.0.1 + 0.61.0 + + + + + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus-spring-boot.version} + + + + com.baomidou + mybatis-plus-annotation + ${mybatis-plus-spring-boot.version} + + + + + org.springframework + spring-framework-bom + ${spring-framework.version} + pom + import + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + ch.qos.logback + logback-core + ${logback.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + org.apache.tomcat.embed + tomcat-embed-core + ${tomcat.version} + + + + org.apache.tomcat.embed + tomcat-embed-el + ${tomcat.version} + + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${tomcat.version} + + + + + com.github.tobato + fastdfs-client + ${tobato.version} + + + + + com.github.xiaoymin + knife4j-openapi3-spring-boot-starter + ${knife4j.version} + + + + io.swagger.core.v3 + swagger-annotations + ${swagger.version} + + + + + pro.fessional + kaptcha + ${kaptcha.version} + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper.boot.version} + + + + + commons-io + commons-io + ${commons.io.version} + + + + + org.apache.poi + poi-ooxml + ${poi.version} + + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + + + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson.version} + + + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + + + com.alibaba + transmittable-thread-local + ${transmittable-thread-local.version} + + + + + com.storm + storm-common-core + ${storm.version} + + + + + com.storm + storm-common-swagger + ${storm.version} + + + + + com.storm + storm-common-security + ${storm.version} + + + + + com.storm + storm-common-sensitive + ${storm.version} + + + + + com.storm + storm-common-datascope + ${storm.version} + + + + + com.storm + storm-common-datasource + ${storm.version} + + + + + com.storm + storm-common-log + ${storm.version} + + + + + com.storm + storm-common-redis + ${storm.version} + + + + + com.storm + storm-api-system + ${storm.version} + + + + + org.projectlombok + lombok + ${lombok.version} + + + + + com.huaweicloud + esdk-obs-java + ${huaweicloud.obs.version} + + + + + com.huaweicloud.sdk + huaweicloud-sdk-core + ${huaweicloud.sdk.version} + + + com.huaweicloud.sdk + huaweicloud-sdk-iotda + ${huaweicloud.sdk.version} + + + + + cn.hutool + hutool-all + ${hutool-all.version} + + + + org.springframework.boot + spring-boot-starter-test + ${springboottest.version} + + + + javax.servlet + javax.servlet-api + ${javax.servlet.version} + + + + + org.apache.qpid + qpid-jms-client + ${amqp.version} + + + + + + storm-auth + storm-gateway + storm-visual + storm-modules + storm-api + storm-common + storm-device + + pom + + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + + + + + public + aliyun nexus + https://maven.aliyun.com/repository/public + + true + + + + + + + public + aliyun nexus + https://maven.aliyun.com/repository/public + + true + + + false + + + + + \ No newline at end of file diff --git a/sql/Device.sql b/sql/Device.sql new file mode 100644 index 0000000..b60d6f5 --- /dev/null +++ b/sql/Device.sql @@ -0,0 +1,191 @@ +-- 创建数据库 +CREATE DATABASE IF NOT EXISTS `storm-device`; +USE `storm-device`; +SET NAMES utf8mb4; + +-- 设备表(主表) +CREATE TABLE device ( + id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + device_id VARCHAR(255) NOT NULL COMMENT '设备ID', + device_name VARCHAR(255) DEFAULT NULL COMMENT '设备名称', + product_id VARCHAR(255) DEFAULT NULL COMMENT '产品ID', + product_name VARCHAR(255) DEFAULT NULL COMMENT '产品名称', + device_type INT DEFAULT NULL COMMENT '设备类型(1:RS-LL-X1, 2:RS-LL-X2)', + STATUS VARCHAR(50) DEFAULT 'OFFLINE' COMMENT '设备在线状态(ONLINE/OFFLINE)', + + -- 位置信息 + location VARCHAR(255) DEFAULT NULL COMMENT '安装位置', + shop_id VARCHAR(255) DEFAULT NULL COMMENT '店铺ID', + longitude VARCHAR(255) DEFAULT NULL COMMENT '经度', + latitude VARCHAR(255) DEFAULT NULL COMMENT '纬度', + + -- 版本信息 + software_version VARCHAR(255) DEFAULT NULL COMMENT '软件版本', + vtxdb_version VARCHAR(255) DEFAULT NULL COMMENT '参数服务器版本', + + -- 许可证信息 + activation_code VARCHAR(255) DEFAULT NULL COMMENT '激活码', + serial_number VARCHAR(255) DEFAULT NULL COMMENT '序列号', + license_status VARCHAR(50) DEFAULT NULL COMMENT '许可证状态', + license_expire_time DATETIME COMMENT '许可证过期时间', + last_massage_time DATETIME COMMENT '最后消息时间', + + -- 在线状态时间记录 + last_online_time DATETIME COMMENT '最后上线时间', + last_offline_time DATETIME COMMENT '最后下线时间', + activation_time DATETIME COMMENT '激活时间', + + -- 运行时长统计(秒) + total_online_duration BIGINT DEFAULT 0 COMMENT '累计在线时长(秒)', + daily_duration BIGINT DEFAULT 0 COMMENT '今日运行时长(秒)', + weekly_duration BIGINT DEFAULT 0 COMMENT '本周运行时长(秒)', + monthly_duration BIGINT DEFAULT 0 COMMENT '本月运行时长(秒)', + + -- 描述和备注 + description TEXT COMMENT '设备描述', + remark TEXT COMMENT '备注', + + -- 系统字段 + is_deleted TINYINT(1) DEFAULT '0' COMMENT '是否删除(0-未删除, 1-已删除)', + create_by VARCHAR(255) DEFAULT NULL COMMENT '创建人', + update_by VARCHAR(255) DEFAULT NULL COMMENT '更新人', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + + PRIMARY KEY (id), + UNIQUE KEY uk_device_id (device_id), + KEY idx_device_id (device_id), + KEY idx_product_id (product_id), + KEY idx_shop_id (shop_id), + KEY idx_status (STATUS), + KEY idx_create_time (create_time), + KEY idx_license_status (license_status), + KEY idx_last_online_time (last_online_time) +) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备表'; + +-- 设备状态日志表 +CREATE TABLE device_status_log ( + id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + device_id VARCHAR(255) NOT NULL COMMENT '设备ID', + STATUS VARCHAR(255) DEFAULT NULL COMMENT '状态', + event_type VARCHAR(255) DEFAULT NULL COMMENT '事件类型', + + -- 毫秒时间戳字段 + massage_start_time BIGINT DEFAULT NULL COMMENT '按摩开始时间(毫秒时间戳)', + massage_end_time BIGINT DEFAULT NULL COMMENT '按摩结束时间(毫秒时间戳)', + massage_duration BIGINT DEFAULT NULL COMMENT '按摩持续时间(毫秒)', + + -- 按摩相关信息 + head_type VARCHAR(255) DEFAULT NULL COMMENT '按摩头类型', + body_part VARCHAR(255) DEFAULT NULL COMMENT '身体部位', + massage_plan VARCHAR(255) DEFAULT NULL COMMENT '按摩方案', + + -- 时间字段 + event_time DATETIME DEFAULT NULL COMMENT '事件时间', + last_online_time DATETIME DEFAULT NULL COMMENT '最后在线时间', + duration INT(11) DEFAULT NULL COMMENT '持续时间(秒)', + + -- 系统字段 + is_deleted TINYINT(1) DEFAULT '0' COMMENT '是否删除(0-未删除, 1-已删除)', + create_by VARCHAR(255) DEFAULT NULL COMMENT '创建人', + update_by VARCHAR(255) DEFAULT NULL COMMENT '更新人', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + + PRIMARY KEY (id), + UNIQUE KEY uk_device_event (device_id, event_time, STATUS), + KEY idx_device_id (device_id), + KEY idx_event_time (event_time), + KEY idx_status (STATUS), + KEY idx_head_type (head_type), + KEY idx_event_type (event_type) +) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备状态日志表'; + +-- 按摩任务表 +CREATE TABLE massage_task ( + id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + device_id VARCHAR(255) NOT NULL COMMENT '设备ID', + device_name VARCHAR(255) DEFAULT NULL COMMENT '设备名称', + product_name VARCHAR(255) DEFAULT NULL COMMENT '产品名称', + head_type VARCHAR(255) DEFAULT NULL COMMENT '按摩头类型', + body_part VARCHAR(255) DEFAULT NULL COMMENT '身体部位', + massage_plan VARCHAR(255) DEFAULT NULL COMMENT '按摩方案', + + -- 创建人 + create_by VARCHAR(255) DEFAULT NULL COMMENT '创建人', + + -- 毫秒时间戳字段 + start_time BIGINT DEFAULT NULL COMMENT '开始时间(毫秒时间戳)', + end_time BIGINT DEFAULT NULL COMMENT '结束时间(毫秒时间戳)', + task_time BIGINT DEFAULT NULL COMMENT '任务时长(毫秒)', + create_timestamp BIGINT DEFAULT NULL COMMENT '创建时间戳(毫秒)', + + -- 时间字段 + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + + -- 系统字段 + is_deleted TINYINT(1) DEFAULT '0' COMMENT '是否删除(0-未删除, 1-已删除)', + update_by VARCHAR(255) DEFAULT NULL COMMENT '更新人', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + + PRIMARY KEY (id), + UNIQUE KEY uk_device_task (device_id, start_time, head_type, body_part), + KEY idx_device_id (device_id), + KEY idx_start_time (start_time), + KEY idx_head_type (head_type), + KEY idx_body_part (body_part), + KEY idx_create_time (create_time) +) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='按摩任务表'; + +-- 设备运行时长统计表 +CREATE TABLE device_runtime_stats ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + device_id VARCHAR(255) NOT NULL COMMENT '设备唯一标识', + stat_date DATE NOT NULL COMMENT '统计日期', + daily_duration BIGINT DEFAULT 0 COMMENT '日运行时长(秒)', + weekly_duration BIGINT DEFAULT 0 COMMENT '周运行时长(秒)', + monthly_duration BIGINT DEFAULT 0 COMMENT '月运行时长(秒)', + online_count INT DEFAULT 0 COMMENT '上线次数', + + -- 系统字段 + is_deleted TINYINT(1) DEFAULT '0' COMMENT '是否删除(0-未删除, 1-已删除)', + create_by VARCHAR(255) DEFAULT NULL COMMENT '创建人', + update_by VARCHAR(255) DEFAULT NULL COMMENT '更新人', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + + UNIQUE KEY uk_device_date (device_id, stat_date), + KEY idx_stat_date (stat_date), + KEY idx_device_id (device_id) +) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备运行时长统计表'; + +-- 设备按头使用统计表 +CREATE TABLE device_head_usage ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + device_id VARCHAR(255) NOT NULL COMMENT '设备唯一标识', + head_type VARCHAR(100) NOT NULL COMMENT '按头类型', + usage_count INT DEFAULT 0 COMMENT '使用次数', + total_duration BIGINT DEFAULT 0 COMMENT '总使用时长(秒)', + daily_duration BIGINT DEFAULT 0 COMMENT '今日使用时长(秒)', + weekly_duration BIGINT DEFAULT 0 COMMENT '本周使用时长(秒)', + monthly_duration BIGINT DEFAULT 0 COMMENT '本月使用时长(秒)', + last_used_time DATETIME COMMENT '最后使用时间', + + -- 系统字段 + is_deleted TINYINT(1) DEFAULT '0' COMMENT '是否删除(0-未删除, 1-已删除)', + create_by VARCHAR(255) DEFAULT NULL COMMENT '创建人', + update_by VARCHAR(255) DEFAULT NULL COMMENT '更新人', + create_time DATETIME DEFAULT NULL COMMENT '创建时间', + update_time DATETIME DEFAULT NULL COMMENT '更新时间', + + UNIQUE KEY uk_device_head (device_id, head_type), + KEY idx_head_type (head_type), + KEY idx_last_used_time (last_used_time), + KEY idx_device_id (device_id) +) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备按头使用统计表'; + +-- 如果业务允许,可以考虑调整唯一键约束,增加时间容差 +-- 比如允许同一设备在同一分钟内只有一条相同状态的记录 +ALTER TABLE device_status_log +DROP INDEX uk_device_event, +ADD UNIQUE KEY uk_device_event_minute (device_id, DATE_FORMAT(event_time, '%Y-%m-%d %H:%i'), status); \ No newline at end of file diff --git a/sql/ry_20250523.sql b/sql/ry_20250523.sql new file mode 100644 index 0000000..048f88f --- /dev/null +++ b/sql/ry_20250523.sql @@ -0,0 +1,870 @@ +create database if not exists storm; +use storm; +SET NAMES utf8mb4; + +-- ---------------------------- +-- 1、部门表 +-- ---------------------------- +drop table if exists sys_dept; +create table sys_dept ( + dept_id bigint(20) not null auto_increment comment '部门id', + parent_id bigint(20) default 0 comment '父部门id', + ancestors varchar(50) default '' comment '祖级列表', + dept_name varchar(30) default '' comment '部门名称', + order_num int(4) default 0 comment '显示顺序', + leader varchar(20) default null comment '负责人', + phone varchar(11) default null comment '联系电话', + email varchar(50) default null comment '邮箱', + status char(1) default '0' comment '部门状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + primary key (dept_id) +) engine=innodb auto_increment=200 comment = '部门表'; + +-- ---------------------------- +-- 初始化-部门表数据 +-- ---------------------------- +insert into sys_dept values(101, 100, '0,100', '深圳总公司', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(102, 100, '0,100', '广州分公司', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(103, 101, '0,100,101', '研发部门', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(104, 101, '0,100,101', '市场部门', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(105, 101, '0,100,101', '测试部门', 3, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(106, 101, '0,100,101', '财务部门', 4, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(107, 101, '0,100,101', '运维部门', 5, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(108, 102, '0,100,102', '市场部门', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(109, 102, '0,100,102', '财务部门', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); + + +-- ---------------------------- +-- 2、用户信息表 +-- ---------------------------- +drop table if exists sys_user; +create table sys_user ( + user_id bigint(20) not null auto_increment comment '用户ID', + dept_id bigint(20) default null comment '部门ID', + user_name varchar(30) not null comment '用户账号', + nick_name varchar(30) not null comment '用户昵称', + user_type varchar(2) default '00' comment '用户类型(00系统用户)', + email varchar(50) default '' comment '用户邮箱', + phonenumber varchar(11) default '' comment '手机号码', + sex char(1) default '0' comment '用户性别(0男 1女 2未知)', + avatar varchar(100) default '' comment '头像地址', + password varchar(100) default '' comment '密码', + status char(1) default '0' comment '账号状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + login_ip varchar(128) default '' comment '最后登录IP', + login_date datetime comment '最后登录时间', + pwd_update_date datetime comment '密码最后更新时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (user_id) +) engine=innodb auto_increment=100 comment = '用户信息表'; + +-- ---------------------------- +-- 初始化-用户信息表数据 +-- ---------------------------- +insert into sys_user values(1, 103, 'admin', 'admin', '00', 'admin@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), sysdate(), 'admin', sysdate(), '', null, '管理员'); +insert into sys_user values(2, 105, 'dz', '丁真', '00', 'dz@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), sysdate(), 'admin', sysdate(), '', null, '测试员'); + + +-- ---------------------------- +-- 3、岗位信息表 +-- ---------------------------- +drop table if exists sys_post; +create table sys_post +( + post_id bigint(20) not null auto_increment comment '岗位ID', + post_code varchar(64) not null comment '岗位编码', + post_name varchar(50) not null comment '岗位名称', + post_sort int(4) not null comment '显示顺序', + status char(1) not null comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (post_id) +) engine=innodb comment = '岗位信息表'; + +-- ---------------------------- +-- 初始化-岗位信息表数据 +-- ---------------------------- +insert into sys_post values(1, 'ceo', '董事长', 1, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(2, 'se', '项目经理', 2, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(3, 'hr', '人力资源', 3, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(4, 'user', '普通员工', 4, '0', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 4、角色信息表 +-- ---------------------------- +drop table if exists sys_role; +create table sys_role ( + role_id bigint(20) not null auto_increment comment '角色ID', + role_name varchar(30) not null comment '角色名称', + role_key varchar(100) not null comment '角色权限字符串', + role_sort int(4) not null comment '显示顺序', + data_scope char(1) default '1' comment '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)', + menu_check_strictly tinyint(1) default 1 comment '菜单树选择项是否关联显示', + dept_check_strictly tinyint(1) default 1 comment '部门树选择项是否关联显示', + status char(1) not null comment '角色状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (role_id) +) engine=innodb auto_increment=100 comment = '角色信息表'; + +-- ---------------------------- +-- 初始化-角色信息表数据 +-- ---------------------------- +insert into sys_role values('1', '超级管理员', 'admin', 1, 1, 1, 1, '0', '0', 'admin', sysdate(), '', null, '超级管理员'); +insert into sys_role values('2', '普通角色', 'common', 2, 2, 1, 1, '0', '0', 'admin', sysdate(), '', null, '普通角色'); + + +-- ---------------------------- +-- 5、菜单权限表 +-- ---------------------------- +drop table if exists sys_menu; +create table sys_menu ( + menu_id bigint(20) not null auto_increment comment '菜单ID', + menu_name varchar(50) not null comment '菜单名称', + parent_id bigint(20) default 0 comment '父菜单ID', + order_num int(4) default 0 comment '显示顺序', + path varchar(200) default '' comment '路由地址', + component varchar(255) default null comment '组件路径', + query varchar(255) default null comment '路由参数', + route_name varchar(50) default '' comment '路由名称', + is_frame int(1) default 1 comment '是否为外链(0是 1否)', + is_cache int(1) default 0 comment '是否缓存(0缓存 1不缓存)', + menu_type char(1) default '' comment '菜单类型(M目录 C菜单 F按钮)', + visible char(1) default 0 comment '菜单状态(0显示 1隐藏)', + status char(1) default 0 comment '菜单状态(0正常 1停用)', + perms varchar(100) default null comment '权限标识', + icon varchar(100) default '#' comment '菜单图标', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default '' comment '备注', + primary key (menu_id) +) engine=innodb auto_increment=2000 comment = '菜单权限表'; + +-- ---------------------------- +-- 初始化-菜单信息表数据 +-- ---------------------------- +-- 一级菜单 +insert into sys_menu values('1', '系统管理', '0', '1', 'system', null, '', '', 1, 0, 'M', '0', '0', '', 'system', 'admin', sysdate(), '', null, '系统管理目录'); +insert into sys_menu values('2', '系统监控', '0', '2', 'monitor', null, '', '', 1, 0, 'M', '0', '0', '', 'monitor', 'admin', sysdate(), '', null, '系统监控目录'); +insert into sys_menu values('3', '系统工具', '0', '3', 'tool', null, '', '', 1, 0, 'M', '0', '0', '', 'tool', 'admin', sysdate(), '', null, '系统工具目录'); +-- 二级菜单 +insert into sys_menu values('100', '用户管理', '1', '1', 'user', 'system/user/index', '', '', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'admin', sysdate(), '', null, '用户管理菜单'); +insert into sys_menu values('101', '角色管理', '1', '2', 'role', 'system/role/index', '', '', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'admin', sysdate(), '', null, '角色管理菜单'); +insert into sys_menu values('102', '菜单管理', '1', '3', 'menu', 'system/menu/index', '', '', 1, 0, 'C', '0', '0', 'system:menu:list', 'tree-table', 'admin', sysdate(), '', null, '菜单管理菜单'); +insert into sys_menu values('103', '部门管理', '1', '4', 'dept', 'system/dept/index', '', '', 1, 0, 'C', '0', '0', 'system:dept:list', 'tree', 'admin', sysdate(), '', null, '部门管理菜单'); +insert into sys_menu values('104', '岗位管理', '1', '5', 'post', 'system/post/index', '', '', 1, 0, 'C', '0', '0', 'system:post:list', 'post', 'admin', sysdate(), '', null, '岗位管理菜单'); +insert into sys_menu values('105', '字典管理', '1', '6', 'dict', 'system/dict/index', '', '', 1, 0, 'C', '0', '0', 'system:dict:list', 'dict', 'admin', sysdate(), '', null, '字典管理菜单'); +insert into sys_menu values('106', '参数设置', '1', '7', 'config', 'system/config/index', '', '', 1, 0, 'C', '0', '0', 'system:config:list', 'edit', 'admin', sysdate(), '', null, '参数设置菜单'); +insert into sys_menu values('107', '通知公告', '1', '8', 'notice', 'system/notice/index', '', '', 1, 0, 'C', '0', '0', 'system:notice:list', 'message', 'admin', sysdate(), '', null, '通知公告菜单'); +insert into sys_menu values('108', '日志管理', '1', '9', 'log', '', '', '', 1, 0, 'M', '0', '0', '', 'log', 'admin', sysdate(), '', null, '日志管理菜单'); +insert into sys_menu values('109', '在线用户', '2', '1', 'online', 'monitor/online/index', '', '', 1, 0, 'C', '0', '0', 'monitor:online:list', 'online', 'admin', sysdate(), '', null, '在线用户菜单'); +insert into sys_menu values('110', '定时任务', '2', '2', 'job', 'monitor/job/index', '', '', 1, 0, 'C', '0', '0', 'monitor:job:list', 'job', 'admin', sysdate(), '', null, '定时任务菜单'); +insert into sys_menu values('111', 'Sentinel控制台', '2', '3', 'http://localhost:8718', '', '', '', 0, 0, 'C', '0', '0', 'monitor:sentinel:list', 'sentinel', 'admin', sysdate(), '', null, '流量控制菜单'); +insert into sys_menu values('112', 'Nacos控制台', '2', '4', 'http://localhost:8848/nacos', '', '', '', 0, 0, 'C', '0', '0', 'monitor:nacos:list', 'nacos', 'admin', sysdate(), '', null, '服务治理菜单'); +insert into sys_menu values('113', 'Admin控制台', '2', '5', 'http://localhost:9100/login', '', '', '', 0, 0, 'C', '0', '0', 'monitor:server:list', 'server', 'admin', sysdate(), '', null, '服务监控菜单'); +insert into sys_menu values('114', '表单构建', '3', '1', 'build', 'tool/build/index', '', '', 1, 0, 'C', '0', '0', 'tool:build:list', 'build', 'admin', sysdate(), '', null, '表单构建菜单'); +insert into sys_menu values('115', '代码生成', '3', '2', 'gen', 'tool/gen/index', '', '', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'admin', sysdate(), '', null, '代码生成菜单'); +insert into sys_menu values('116', '系统接口', '3', '3', 'http://localhost:8080/swagger-ui/index.html', '', '', '', 0, 0, 'C', '0', '0', 'tool:swagger:list', 'swagger', 'admin', sysdate(), '', null, '系统接口菜单'); +-- 三级菜单 +insert into sys_menu values('500', '操作日志', '108', '1', 'operlog', 'system/operlog/index', '', '', 1, 0, 'C', '0', '0', 'system:operlog:list', 'form', 'admin', sysdate(), '', null, '操作日志菜单'); +insert into sys_menu values('501', '登录日志', '108', '2', 'logininfor', 'system/logininfor/index', '', '', 1, 0, 'C', '0', '0', 'system:logininfor:list', 'logininfor', 'admin', sysdate(), '', null, '登录日志菜单'); +-- 用户管理按钮 +insert into sys_menu values('1000', '用户查询', '100', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1001', '用户新增', '100', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1002', '用户修改', '100', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1003', '用户删除', '100', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1004', '用户导出', '100', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:export', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1005', '用户导入', '100', '6', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:import', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1006', '重置密码', '100', '7', '', '', '', '', 1, 0, 'F', '0', '0', 'system:user:resetPwd', '#', 'admin', sysdate(), '', null, ''); +-- 角色管理按钮 +insert into sys_menu values('1007', '角色查询', '101', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1008', '角色新增', '101', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1009', '角色修改', '101', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1010', '角色删除', '101', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1011', '角色导出', '101', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:role:export', '#', 'admin', sysdate(), '', null, ''); +-- 菜单管理按钮 +insert into sys_menu values('1012', '菜单查询', '102', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1013', '菜单新增', '102', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1014', '菜单修改', '102', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1015', '菜单删除', '102', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:menu:remove', '#', 'admin', sysdate(), '', null, ''); +-- 部门管理按钮 +insert into sys_menu values('1016', '部门查询', '103', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1017', '部门新增', '103', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1018', '部门修改', '103', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1019', '部门删除', '103', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:dept:remove', '#', 'admin', sysdate(), '', null, ''); +-- 岗位管理按钮 +insert into sys_menu values('1020', '岗位查询', '104', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1021', '岗位新增', '104', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1022', '岗位修改', '104', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1023', '岗位删除', '104', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1024', '岗位导出', '104', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'system:post:export', '#', 'admin', sysdate(), '', null, ''); +-- 字典管理按钮 +insert into sys_menu values('1025', '字典查询', '105', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1026', '字典新增', '105', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1027', '字典修改', '105', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1028', '字典删除', '105', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1029', '字典导出', '105', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:dict:export', '#', 'admin', sysdate(), '', null, ''); +-- 参数设置按钮 +insert into sys_menu values('1030', '参数查询', '106', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1031', '参数新增', '106', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1032', '参数修改', '106', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1033', '参数删除', '106', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1034', '参数导出', '106', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:config:export', '#', 'admin', sysdate(), '', null, ''); +-- 通知公告按钮 +insert into sys_menu values('1035', '公告查询', '107', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1036', '公告新增', '107', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1037', '公告修改', '107', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1038', '公告删除', '107', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:notice:remove', '#', 'admin', sysdate(), '', null, ''); +-- 操作日志按钮 +insert into sys_menu values('1039', '操作查询', '500', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:operlog:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1040', '操作删除', '500', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:operlog:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1041', '日志导出', '500', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:operlog:export', '#', 'admin', sysdate(), '', null, ''); +-- 登录日志按钮 +insert into sys_menu values('1042', '登录查询', '501', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:logininfor:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1043', '登录删除', '501', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:logininfor:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1044', '日志导出', '501', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:logininfor:export', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1045', '账户解锁', '501', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'system:logininfor:unlock', '#', 'admin', sysdate(), '', null, ''); +-- 在线用户按钮 +insert into sys_menu values('1046', '在线查询', '109', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1047', '批量强退', '109', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:batchLogout', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1048', '单条强退', '109', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:online:forceLogout', '#', 'admin', sysdate(), '', null, ''); +-- 定时任务按钮 +insert into sys_menu values('1049', '任务查询', '110', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1050', '任务新增', '110', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1051', '任务修改', '110', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1052', '任务删除', '110', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1053', '状态修改', '110', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:changeStatus', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1054', '任务导出', '110', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'monitor:job:export', '#', 'admin', sysdate(), '', null, ''); +-- 代码生成按钮 +insert into sys_menu values('1055', '生成查询', '115', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1056', '生成修改', '115', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1057', '生成删除', '115', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1058', '导入代码', '115', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1059', '预览代码', '115', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1060', '生成代码', '115', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 6、用户和角色关联表 用户N-1角色 +-- ---------------------------- +drop table if exists sys_user_role; +create table sys_user_role ( + user_id bigint(20) not null comment '用户ID', + role_id bigint(20) not null comment '角色ID', + primary key(user_id, role_id) +) engine=innodb comment = '用户和角色关联表'; + +-- ---------------------------- +-- 初始化-用户和角色关联表数据 +-- ---------------------------- +insert into sys_user_role values ('1', '1'); +insert into sys_user_role values ('2', '2'); + + +-- ---------------------------- +-- 7、角色和菜单关联表 角色1-N菜单 +-- ---------------------------- +drop table if exists sys_role_menu; +create table sys_role_menu ( + role_id bigint(20) not null comment '角色ID', + menu_id bigint(20) not null comment '菜单ID', + primary key(role_id, menu_id) +) engine=innodb comment = '角色和菜单关联表'; + +-- ---------------------------- +-- 初始化-角色和菜单关联表数据 +-- ---------------------------- +insert into sys_role_menu values ('2', '1'); +insert into sys_role_menu values ('2', '2'); +insert into sys_role_menu values ('2', '3'); +insert into sys_role_menu values ('2', '4'); +insert into sys_role_menu values ('2', '100'); +insert into sys_role_menu values ('2', '101'); +insert into sys_role_menu values ('2', '102'); +insert into sys_role_menu values ('2', '103'); +insert into sys_role_menu values ('2', '104'); +insert into sys_role_menu values ('2', '105'); +insert into sys_role_menu values ('2', '106'); +insert into sys_role_menu values ('2', '107'); +insert into sys_role_menu values ('2', '108'); +insert into sys_role_menu values ('2', '109'); +insert into sys_role_menu values ('2', '110'); +insert into sys_role_menu values ('2', '111'); +insert into sys_role_menu values ('2', '112'); +insert into sys_role_menu values ('2', '113'); +insert into sys_role_menu values ('2', '114'); +insert into sys_role_menu values ('2', '115'); +insert into sys_role_menu values ('2', '116'); +insert into sys_role_menu values ('2', '500'); +insert into sys_role_menu values ('2', '501'); +insert into sys_role_menu values ('2', '1000'); +insert into sys_role_menu values ('2', '1001'); +insert into sys_role_menu values ('2', '1002'); +insert into sys_role_menu values ('2', '1003'); +insert into sys_role_menu values ('2', '1004'); +insert into sys_role_menu values ('2', '1005'); +insert into sys_role_menu values ('2', '1006'); +insert into sys_role_menu values ('2', '1007'); +insert into sys_role_menu values ('2', '1008'); +insert into sys_role_menu values ('2', '1009'); +insert into sys_role_menu values ('2', '1010'); +insert into sys_role_menu values ('2', '1011'); +insert into sys_role_menu values ('2', '1012'); +insert into sys_role_menu values ('2', '1013'); +insert into sys_role_menu values ('2', '1014'); +insert into sys_role_menu values ('2', '1015'); +insert into sys_role_menu values ('2', '1016'); +insert into sys_role_menu values ('2', '1017'); +insert into sys_role_menu values ('2', '1018'); +insert into sys_role_menu values ('2', '1019'); +insert into sys_role_menu values ('2', '1020'); +insert into sys_role_menu values ('2', '1021'); +insert into sys_role_menu values ('2', '1022'); +insert into sys_role_menu values ('2', '1023'); +insert into sys_role_menu values ('2', '1024'); +insert into sys_role_menu values ('2', '1025'); +insert into sys_role_menu values ('2', '1026'); +insert into sys_role_menu values ('2', '1027'); +insert into sys_role_menu values ('2', '1028'); +insert into sys_role_menu values ('2', '1029'); +insert into sys_role_menu values ('2', '1030'); +insert into sys_role_menu values ('2', '1031'); +insert into sys_role_menu values ('2', '1032'); +insert into sys_role_menu values ('2', '1033'); +insert into sys_role_menu values ('2', '1034'); +insert into sys_role_menu values ('2', '1035'); +insert into sys_role_menu values ('2', '1036'); +insert into sys_role_menu values ('2', '1037'); +insert into sys_role_menu values ('2', '1038'); +insert into sys_role_menu values ('2', '1039'); +insert into sys_role_menu values ('2', '1040'); +insert into sys_role_menu values ('2', '1041'); +insert into sys_role_menu values ('2', '1042'); +insert into sys_role_menu values ('2', '1043'); +insert into sys_role_menu values ('2', '1044'); +insert into sys_role_menu values ('2', '1045'); +insert into sys_role_menu values ('2', '1046'); +insert into sys_role_menu values ('2', '1047'); +insert into sys_role_menu values ('2', '1048'); +insert into sys_role_menu values ('2', '1049'); +insert into sys_role_menu values ('2', '1050'); +insert into sys_role_menu values ('2', '1051'); +insert into sys_role_menu values ('2', '1052'); +insert into sys_role_menu values ('2', '1053'); +insert into sys_role_menu values ('2', '1054'); +insert into sys_role_menu values ('2', '1055'); +insert into sys_role_menu values ('2', '1056'); +insert into sys_role_menu values ('2', '1057'); +insert into sys_role_menu values ('2', '1058'); +insert into sys_role_menu values ('2', '1059'); +insert into sys_role_menu values ('2', '1060'); + +-- ---------------------------- +-- 8、角色和部门关联表 角色1-N部门 +-- ---------------------------- +drop table if exists sys_role_dept; +create table sys_role_dept ( + role_id bigint(20) not null comment '角色ID', + dept_id bigint(20) not null comment '部门ID', + primary key(role_id, dept_id) +) engine=innodb comment = '角色和部门关联表'; + +-- ---------------------------- +-- 初始化-角色和部门关联表数据 +-- ---------------------------- +insert into sys_role_dept values ('2', '100'); +insert into sys_role_dept values ('2', '101'); +insert into sys_role_dept values ('2', '105'); + + +-- ---------------------------- +-- 9、用户与岗位关联表 用户1-N岗位 +-- ---------------------------- +drop table if exists sys_user_post; +create table sys_user_post +( + user_id bigint(20) not null comment '用户ID', + post_id bigint(20) not null comment '岗位ID', + primary key (user_id, post_id) +) engine=innodb comment = '用户与岗位关联表'; + +-- ---------------------------- +-- 初始化-用户与岗位关联表数据 +-- ---------------------------- +insert into sys_user_post values ('1', '1'); +insert into sys_user_post values ('2', '2'); + + +-- ---------------------------- +-- 10、操作日志记录 +-- ---------------------------- +drop table if exists sys_oper_log; +create table sys_oper_log ( + oper_id bigint(20) not null auto_increment comment '日志主键', + title varchar(50) default '' comment '模块标题', + business_type int(2) default 0 comment '业务类型(0其它 1新增 2修改 3删除)', + method varchar(200) default '' comment '方法名称', + request_method varchar(10) default '' comment '请求方式', + operator_type int(1) default 0 comment '操作类别(0其它 1后台用户 2手机端用户)', + oper_name varchar(50) default '' comment '操作人员', + dept_name varchar(50) default '' comment '部门名称', + oper_url varchar(255) default '' comment '请求URL', + oper_ip varchar(128) default '' comment '主机地址', + oper_location varchar(255) default '' comment '操作地点', + oper_param varchar(2000) default '' comment '请求参数', + json_result varchar(2000) default '' comment '返回参数', + status int(1) default 0 comment '操作状态(0正常 1异常)', + error_msg varchar(2000) default '' comment '错误消息', + oper_time datetime comment '操作时间', + cost_time bigint(20) default 0 comment '消耗时间', + primary key (oper_id), + key idx_sys_oper_log_bt (business_type), + key idx_sys_oper_log_s (status), + key idx_sys_oper_log_ot (oper_time) +) engine=innodb auto_increment=100 comment = '操作日志记录'; + + +-- ---------------------------- +-- 11、字典类型表 +-- ---------------------------- +drop table if exists sys_dict_type; +create table sys_dict_type +( + dict_id bigint(20) not null auto_increment comment '字典主键', + dict_name varchar(100) default '' comment '字典名称', + dict_type varchar(100) default '' comment '字典类型', + status char(1) default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (dict_id), + unique (dict_type) +) engine=innodb auto_increment=100 comment = '字典类型表'; + +insert into sys_dict_type values(1, '用户性别', 'sys_user_sex', '0', 'admin', sysdate(), '', null, '用户性别列表'); +insert into sys_dict_type values(2, '菜单状态', 'sys_show_hide', '0', 'admin', sysdate(), '', null, '菜单状态列表'); +insert into sys_dict_type values(3, '系统开关', 'sys_normal_disable', '0', 'admin', sysdate(), '', null, '系统开关列表'); +insert into sys_dict_type values(4, '任务状态', 'sys_job_status', '0', 'admin', sysdate(), '', null, '任务状态列表'); +insert into sys_dict_type values(5, '任务分组', 'sys_job_group', '0', 'admin', sysdate(), '', null, '任务分组列表'); +insert into sys_dict_type values(6, '系统是否', 'sys_yes_no', '0', 'admin', sysdate(), '', null, '系统是否列表'); +insert into sys_dict_type values(7, '通知类型', 'sys_notice_type', '0', 'admin', sysdate(), '', null, '通知类型列表'); +insert into sys_dict_type values(8, '通知状态', 'sys_notice_status', '0', 'admin', sysdate(), '', null, '通知状态列表'); +insert into sys_dict_type values(9, '操作类型', 'sys_oper_type', '0', 'admin', sysdate(), '', null, '操作类型列表'); +insert into sys_dict_type values(10, '系统状态', 'sys_common_status', '0', 'admin', sysdate(), '', null, '登录状态列表'); + + +-- ---------------------------- +-- 12、字典数据表 +-- ---------------------------- +drop table if exists sys_dict_data; +create table sys_dict_data +( + dict_code bigint(20) not null auto_increment comment '字典编码', + dict_sort int(4) default 0 comment '字典排序', + dict_label varchar(100) default '' comment '字典标签', + dict_value varchar(100) default '' comment '字典键值', + dict_type varchar(100) default '' comment '字典类型', + css_class varchar(100) default null comment '样式属性(其他样式扩展)', + list_class varchar(100) default null comment '表格回显样式', + is_default char(1) default 'N' comment '是否默认(Y是 N否)', + status char(1) default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (dict_code) +) engine=innodb auto_increment=100 comment = '字典数据表'; + +insert into sys_dict_data values(1, 1, '男', '0', 'sys_user_sex', '', '', 'Y', '0', 'admin', sysdate(), '', null, '性别男'); +insert into sys_dict_data values(2, 2, '女', '1', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别女'); +insert into sys_dict_data values(3, 3, '未知', '2', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别未知'); +insert into sys_dict_data values(4, 1, '显示', '0', 'sys_show_hide', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '显示菜单'); +insert into sys_dict_data values(5, 2, '隐藏', '1', 'sys_show_hide', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '隐藏菜单'); +insert into sys_dict_data values(6, 1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(7, 2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); +insert into sys_dict_data values(8, 1, '正常', '0', 'sys_job_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(9, 2, '暂停', '1', 'sys_job_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); +insert into sys_dict_data values(10, 1, '默认', 'DEFAULT', 'sys_job_group', '', '', 'Y', '0', 'admin', sysdate(), '', null, '默认分组'); +insert into sys_dict_data values(11, 2, '系统', 'SYSTEM', 'sys_job_group', '', '', 'N', '0', 'admin', sysdate(), '', null, '系统分组'); +insert into sys_dict_data values(12, 1, '是', 'Y', 'sys_yes_no', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '系统默认是'); +insert into sys_dict_data values(13, 2, '否', 'N', 'sys_yes_no', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '系统默认否'); +insert into sys_dict_data values(14, 1, '通知', '1', 'sys_notice_type', '', 'warning', 'Y', '0', 'admin', sysdate(), '', null, '通知'); +insert into sys_dict_data values(15, 2, '公告', '2', 'sys_notice_type', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '公告'); +insert into sys_dict_data values(16, 1, '正常', '0', 'sys_notice_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(17, 2, '关闭', '1', 'sys_notice_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '关闭状态'); +insert into sys_dict_data values(18, 99, '其他', '0', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '其他操作'); +insert into sys_dict_data values(19, 1, '新增', '1', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '新增操作'); +insert into sys_dict_data values(20, 2, '修改', '2', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '修改操作'); +insert into sys_dict_data values(21, 3, '删除', '3', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '删除操作'); +insert into sys_dict_data values(22, 4, '授权', '4', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '授权操作'); +insert into sys_dict_data values(23, 5, '导出', '5', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导出操作'); +insert into sys_dict_data values(24, 6, '导入', '6', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导入操作'); +insert into sys_dict_data values(25, 7, '强退', '7', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '强退操作'); +insert into sys_dict_data values(26, 8, '生成代码', '8', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '生成操作'); +insert into sys_dict_data values(27, 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '清空操作'); +insert into sys_dict_data values(28, 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(29, 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); + + +-- ---------------------------- +-- 13、参数配置表 +-- ---------------------------- +drop table if exists sys_config; +create table sys_config ( + config_id int(5) not null auto_increment comment '参数主键', + config_name varchar(100) default '' comment '参数名称', + config_key varchar(100) default '' comment '参数键名', + config_value varchar(500) default '' comment '参数键值', + config_type char(1) default 'N' comment '系统内置(Y是 N否)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (config_id) +) engine=innodb auto_increment=100 comment = '参数配置表'; + +insert into sys_config values(1, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'admin', sysdate(), '', null, '蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow' ); +insert into sys_config values(2, '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', 'admin', sysdate(), '', null, '初始化密码 123456' ); +insert into sys_config values(3, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', sysdate(), '', null, '深色主题theme-dark,浅色主题theme-light' ); +insert into sys_config values(4, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', sysdate(), '', null, '是否开启注册用户功能(true开启,false关闭)'); +insert into sys_config values(5, '用户登录-黑名单列表', 'sys.login.blackIPList', '', 'Y', 'admin', sysdate(), '', null, '设置登录IP黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)'); +insert into sys_config values(6, '用户管理-初始密码修改策略', 'sys.account.initPasswordModify', '1', 'Y', 'admin', sysdate(), '', null, '0:初始密码修改策略关闭,没有任何提示,1:提醒用户,如果未修改初始密码,则在登录时就会提醒修改密码对话框'); +insert into sys_config values(7, '用户管理-账号密码更新周期', 'sys.account.passwordValidateDays', '0', 'Y', 'admin', sysdate(), '', null, '密码更新周期(填写数字,数据初始化值为0不限制,若修改必须为大于0小于365的正整数),如果超过这个周期登录系统时,则在登录时就会提醒修改密码对话框'); + + +-- ---------------------------- +-- 14、系统访问记录 +-- ---------------------------- +drop table if exists sys_logininfor; +create table sys_logininfor ( + info_id bigint(20) not null auto_increment comment '访问ID', + user_name varchar(50) default '' comment '用户账号', + ipaddr varchar(128) default '' comment '登录IP地址', + status char(1) default '0' comment '登录状态(0成功 1失败)', + msg varchar(255) default '' comment '提示信息', + access_time datetime comment '访问时间', + primary key (info_id), + key idx_sys_logininfor_s (status), + key idx_sys_logininfor_lt (access_time) +) engine=innodb auto_increment=100 comment = '系统访问记录'; + + +-- ---------------------------- +-- 15、定时任务调度表 +-- ---------------------------- +drop table if exists sys_job; +create table sys_job ( + job_id bigint(20) not null auto_increment comment '任务ID', + job_name varchar(64) default '' comment '任务名称', + job_group varchar(64) default 'DEFAULT' comment '任务组名', + invoke_target varchar(500) not null comment '调用目标字符串', + cron_expression varchar(255) default '' comment 'cron执行表达式', + misfire_policy varchar(20) default '3' comment '计划执行错误策略(1立即执行 2执行一次 3放弃执行)', + concurrent char(1) default '1' comment '是否并发执行(0允许 1禁止)', + status char(1) default '0' comment '状态(0正常 1暂停)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default '' comment '备注信息', + primary key (job_id, job_name, job_group) +) engine=innodb auto_increment=100 comment = '定时任务调度表'; + +insert into sys_job values(1, '系统默认(无参)', 'DEFAULT', 'ryTask.ryNoParams', '0/10 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); +insert into sys_job values(2, '系统默认(有参)', 'DEFAULT', 'ryTask.ryParams(\'ry\')', '0/15 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); +insert into sys_job values(3, '系统默认(多参)', 'DEFAULT', 'ryTask.ryMultipleParams(\'ry\', true, 2000L, 316.50D, 100)', '0/20 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 16、定时任务调度日志表 +-- ---------------------------- +drop table if exists sys_job_log; +create table sys_job_log ( + job_log_id bigint(20) not null auto_increment comment '任务日志ID', + job_name varchar(64) not null comment '任务名称', + job_group varchar(64) not null comment '任务组名', + invoke_target varchar(500) not null comment '调用目标字符串', + job_message varchar(500) comment '日志信息', + status char(1) default '0' comment '执行状态(0正常 1失败)', + exception_info varchar(2000) default '' comment '异常信息', + create_time datetime comment '创建时间', + primary key (job_log_id) +) engine=innodb comment = '定时任务调度日志表'; + + +-- ---------------------------- +-- 17、通知公告表 +-- ---------------------------- +drop table if exists sys_notice; +create table sys_notice ( + notice_id int(4) not null auto_increment comment '公告ID', + notice_title varchar(50) not null comment '公告标题', + notice_type char(1) not null comment '公告类型(1通知 2公告)', + notice_content longblob default null comment '公告内容', + status char(1) default '0' comment '公告状态(0正常 1关闭)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(255) default null comment '备注', + primary key (notice_id) +) engine=innodb auto_increment=10 comment = '通知公告表'; + + + +-- ---------------------------- +-- 18、代码生成业务表 +-- ---------------------------- +drop table if exists gen_table; +create table gen_table ( + table_id bigint(20) not null auto_increment comment '编号', + table_name varchar(200) default '' comment '表名称', + table_comment varchar(500) default '' comment '表描述', + sub_table_name varchar(64) default null comment '关联子表的表名', + sub_table_fk_name varchar(64) default null comment '子表关联的外键名', + class_name varchar(100) default '' comment '实体类名称', + tpl_category varchar(200) default 'crud' comment '使用的模板(crud单表操作 tree树表操作)', + tpl_web_type varchar(30) default '' comment '前端模板类型(element-ui模版 element-plus模版)', + package_name varchar(100) comment '生成包路径', + module_name varchar(30) comment '生成模块名', + business_name varchar(30) comment '生成业务名', + function_name varchar(50) comment '生成功能名', + function_author varchar(50) comment '生成功能作者', + gen_type char(1) default '0' comment '生成代码方式(0zip压缩包 1自定义路径)', + gen_path varchar(200) default '/' comment '生成路径(不填默认项目路径)', + options varchar(1000) comment '其它生成选项', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (table_id) +) engine=innodb auto_increment=1 comment = '代码生成业务表'; + + +-- ---------------------------- +-- 19、代码生成业务表字段 +-- ---------------------------- +drop table if exists gen_table_column; +create table gen_table_column ( + column_id bigint(20) not null auto_increment comment '编号', + table_id bigint(20) comment '归属表编号', + column_name varchar(200) comment '列名称', + column_comment varchar(500) comment '列描述', + column_type varchar(100) comment '列类型', + java_type varchar(500) comment 'JAVA类型', + java_field varchar(200) comment 'JAVA字段名', + is_pk char(1) comment '是否主键(1是)', + is_increment char(1) comment '是否自增(1是)', + is_required char(1) comment '是否必填(1是)', + is_insert char(1) comment '是否为插入字段(1是)', + is_edit char(1) comment '是否编辑字段(1是)', + is_list char(1) comment '是否列表字段(1是)', + is_query char(1) comment '是否查询字段(1是)', + query_type varchar(200) default 'EQ' comment '查询方式(等于、不等于、大于、小于、范围)', + html_type varchar(200) comment '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)', + dict_type varchar(200) default '' comment '字典类型', + sort int comment '排序', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + primary key (column_id) +) engine=innodb auto_increment=1 comment = '代码生成业务表字段'; + +DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; +DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; +DROP TABLE IF EXISTS QRTZ_LOCKS; +DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; +DROP TABLE IF EXISTS QRTZ_CALENDARS; + +-- ---------------------------- +-- 1、存储每一个已配置的 jobDetail 的详细信息 +-- ---------------------------- +create table QRTZ_JOB_DETAILS ( + sched_name varchar(120) not null comment '调度名称', + job_name varchar(200) not null comment '任务名称', + job_group varchar(200) not null comment '任务组名', + description varchar(250) null comment '相关介绍', + job_class_name varchar(250) not null comment '执行任务类名称', + is_durable varchar(1) not null comment '是否持久化', + is_nonconcurrent varchar(1) not null comment '是否并发', + is_update_data varchar(1) not null comment '是否更新数据', + requests_recovery varchar(1) not null comment '是否接受恢复执行', + job_data blob null comment '存放持久化job对象', + primary key (sched_name, job_name, job_group) +) engine=innodb comment = '任务详细信息表'; + +-- ---------------------------- +-- 2、 存储已配置的 Trigger 的信息 +-- ---------------------------- +create table QRTZ_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + trigger_name varchar(200) not null comment '触发器的名字', + trigger_group varchar(200) not null comment '触发器所属组的名字', + job_name varchar(200) not null comment 'qrtz_job_details表job_name的外键', + job_group varchar(200) not null comment 'qrtz_job_details表job_group的外键', + description varchar(250) null comment '相关介绍', + next_fire_time bigint(13) null comment '上一次触发时间(毫秒)', + prev_fire_time bigint(13) null comment '下一次触发时间(默认为-1表示不触发)', + priority integer null comment '优先级', + trigger_state varchar(16) not null comment '触发器状态', + trigger_type varchar(8) not null comment '触发器的类型', + start_time bigint(13) not null comment '开始时间', + end_time bigint(13) null comment '结束时间', + calendar_name varchar(200) null comment '日程表名称', + misfire_instr smallint(2) null comment '补偿执行的策略', + job_data blob null comment '存放持久化job对象', + primary key (sched_name, trigger_name, trigger_group), + foreign key (sched_name, job_name, job_group) references QRTZ_JOB_DETAILS(sched_name, job_name, job_group) +) engine=innodb comment = '触发器详细信息表'; + +-- ---------------------------- +-- 3、 存储简单的 Trigger,包括重复次数,间隔,以及已触发的次数 +-- ---------------------------- +create table QRTZ_SIMPLE_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + repeat_count bigint(7) not null comment '重复的次数统计', + repeat_interval bigint(12) not null comment '重复的间隔时间', + times_triggered bigint(10) not null comment '已经触发的次数', + primary key (sched_name, trigger_name, trigger_group), + foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group) +) engine=innodb comment = '简单触发器的信息表'; + +-- ---------------------------- +-- 4、 存储 Cron Trigger,包括 Cron 表达式和时区信息 +-- ---------------------------- +create table QRTZ_CRON_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + cron_expression varchar(200) not null comment 'cron表达式', + time_zone_id varchar(80) comment '时区', + primary key (sched_name, trigger_name, trigger_group), + foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group) +) engine=innodb comment = 'Cron类型的触发器表'; + +-- ---------------------------- +-- 5、 Trigger 作为 Blob 类型存储(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候) +-- ---------------------------- +create table QRTZ_BLOB_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + blob_data blob null comment '存放持久化Trigger对象', + primary key (sched_name, trigger_name, trigger_group), + foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group) +) engine=innodb comment = 'Blob类型的触发器表'; + +-- ---------------------------- +-- 6、 以 Blob 类型存储存放日历信息, quartz可配置一个日历来指定一个时间范围 +-- ---------------------------- +create table QRTZ_CALENDARS ( + sched_name varchar(120) not null comment '调度名称', + calendar_name varchar(200) not null comment '日历名称', + calendar blob not null comment '存放持久化calendar对象', + primary key (sched_name, calendar_name) +) engine=innodb comment = '日历信息表'; + +-- ---------------------------- +-- 7、 存储已暂停的 Trigger 组的信息 +-- ---------------------------- +create table QRTZ_PAUSED_TRIGGER_GRPS ( + sched_name varchar(120) not null comment '调度名称', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + primary key (sched_name, trigger_group) +) engine=innodb comment = '暂停的触发器表'; + +-- ---------------------------- +-- 8、 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息 +-- ---------------------------- +create table QRTZ_FIRED_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + entry_id varchar(95) not null comment '调度器实例id', + trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + instance_name varchar(200) not null comment '调度器实例名', + fired_time bigint(13) not null comment '触发的时间', + sched_time bigint(13) not null comment '定时器制定的时间', + priority integer not null comment '优先级', + state varchar(16) not null comment '状态', + job_name varchar(200) null comment '任务名称', + job_group varchar(200) null comment '任务组名', + is_nonconcurrent varchar(1) null comment '是否并发', + requests_recovery varchar(1) null comment '是否接受恢复执行', + primary key (sched_name, entry_id) +) engine=innodb comment = '已触发的触发器表'; + +-- ---------------------------- +-- 9、 存储少量的有关 Scheduler 的状态信息,假如是用于集群中,可以看到其他的 Scheduler 实例 +-- ---------------------------- +create table QRTZ_SCHEDULER_STATE ( + sched_name varchar(120) not null comment '调度名称', + instance_name varchar(200) not null comment '实例名称', + last_checkin_time bigint(13) not null comment '上次检查时间', + checkin_interval bigint(13) not null comment '检查间隔时间', + primary key (sched_name, instance_name) +) engine=innodb comment = '调度器状态表'; + +-- ---------------------------- +-- 10、 存储程序的悲观锁的信息(假如使用了悲观锁) +-- ---------------------------- +create table QRTZ_LOCKS ( + sched_name varchar(120) not null comment '调度名称', + lock_name varchar(40) not null comment '悲观锁名称', + primary key (sched_name, lock_name) +) engine=innodb comment = '存储的悲观锁信息表'; + +-- ---------------------------- +-- 11、 Quartz集群实现同步机制的行锁表 +-- ---------------------------- +create table QRTZ_SIMPROP_TRIGGERS ( + sched_name varchar(120) not null comment '调度名称', + trigger_name varchar(200) not null comment 'qrtz_triggers表trigger_name的外键', + trigger_group varchar(200) not null comment 'qrtz_triggers表trigger_group的外键', + str_prop_1 varchar(512) null comment 'String类型的trigger的第一个参数', + str_prop_2 varchar(512) null comment 'String类型的trigger的第二个参数', + str_prop_3 varchar(512) null comment 'String类型的trigger的第三个参数', + int_prop_1 int null comment 'int类型的trigger的第一个参数', + int_prop_2 int null comment 'int类型的trigger的第二个参数', + long_prop_1 bigint null comment 'long类型的trigger的第一个参数', + long_prop_2 bigint null comment 'long类型的trigger的第二个参数', + dec_prop_1 numeric(13,4) null comment 'decimal类型的trigger的第一个参数', + dec_prop_2 numeric(13,4) null comment 'decimal类型的trigger的第二个参数', + bool_prop_1 varchar(1) null comment 'Boolean类型的trigger的第一个参数', + bool_prop_2 varchar(1) null comment 'Boolean类型的trigger的第二个参数', + primary key (sched_name, trigger_name, trigger_group), + foreign key (sched_name, trigger_name, trigger_group) references QRTZ_TRIGGERS(sched_name, trigger_name, trigger_group) +) engine=innodb comment = '同步机制的行锁表'; + +commit; \ No newline at end of file diff --git a/sql/ry_config_20250224.sql b/sql/ry_config_20250224.sql new file mode 100644 index 0000000..b1549b3 --- /dev/null +++ b/sql/ry_config_20250224.sql @@ -0,0 +1,247 @@ +DROP DATABASE IF EXISTS `storm-config`; + +CREATE DATABASE `storm-config` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +USE `storm-config`; + +/******************************************/ +/* 表名称 = config_info */ +/******************************************/ +CREATE TABLE `config_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) DEFAULT NULL COMMENT 'group_id', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COMMENT 'source user', + `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', + `c_desc` varchar(256) DEFAULT NULL COMMENT 'configuration description', + `c_use` varchar(64) DEFAULT NULL COMMENT 'configuration usage', + `effect` varchar(64) DEFAULT NULL COMMENT '配置生效的描述', + `type` varchar(64) DEFAULT NULL COMMENT '配置的类型', + `c_schema` text COMMENT '配置的模式', + `encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info'; + +insert into config_info(id, data_id, group_id, content, md5, gmt_create, gmt_modified, src_user, src_ip, app_name, tenant_id, c_desc, c_use, effect, type, c_schema, encrypted_data_key) values +(1,'application-dev.yml','DEFAULT_GROUP','spring:\n autoconfigure:\n exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure\n\n# feign 配置\nfeign:\n sentinel:\n enabled: true\n okhttp:\n enabled: true\n httpclient:\n enabled: false\n client:\n config:\n default:\n connectTimeout: 10000\n readTimeout: 10000\n compression:\n request:\n enabled: true\n min-request-size: 8192\n response:\n enabled: true\n\n# 暴露监控端点\nmanagement:\n endpoints:\n web:\n exposure:\n include: \'*\'\n','9928f41dfb10386ad38b3254af5692e0','2020-05-20 12:00:00','2024-08-29 12:14:45','nacos','0:0:0:0:0:0:0:1','','','通用配置','null','null','yaml','',''), +(2,'storm-gateway-dev.yml','DEFAULT_GROUP','spring:\n redis:\n host: 113.45.36.253\n port: 6379\n password: 123456\n cloud:\n gateway:\n discovery:\n locator:\n lowerCaseServiceId: true\n enabled: true\n routes:\n # 认证中心\n - id: storm-auth\n uri: lb://storm-auth\n predicates:\n - Path=/auth/**\n filters:\n # 验证码处理\n - CacheRequestBody\n - ValidateCodeFilter\n - StripPrefix=1\n # 代码生成\n - id: storm-gen\n uri: lb://storm-gen\n predicates:\n - Path=/code/**\n filters:\n - StripPrefix=1\n # 定时任务\n - id: storm-job\n uri: lb://storm-job\n predicates:\n - Path=/schedule/**\n filters:\n - StripPrefix=1\n # 系统模块\n - id: storm-system\n uri: lb://storm-system\n predicates:\n - Path=/system/**\n filters:\n - StripPrefix=1\n # 文件服务\n - id: storm-file\n uri: lb://storm-file\n predicates:\n - Path=/file/**\n filters:\n - StripPrefix=1\n\n# 安全配置\nsecurity:\n # 验证码\n captcha:\n enabled: true\n type: math\n # 防止XSS攻击\n xss:\n enabled: true\n excludeUrls:\n - /system/notice\n\n # 不校验白名单\n ignore:\n whites:\n - /auth/logout\n - /auth/login\n - /auth/register\n - /*/v2/api-docs\n - /*/v3/api-docs\n - /csrf\n\n# springdoc配置\nspringdoc:\n webjars:\n # 访问前缀\n prefix:\n','4d329eb08a941a8dd9d26f542c6ac6c5','2020-05-14 14:17:55','2024-09-02 12:13:50','nacos','0:0:0:0:0:0:0:1','','','网关模块','null','null','yaml','',''), +(3,'storm-auth-dev.yml','DEFAULT_GROUP','spring:\n redis:\n host: 113.45.36.253\n port: 6379\n password: 123456\n','a03e7632a0a74520eeb4fbedd6d82d97','2020-11-20 00:00:00','2024-09-02 12:13:58','nacos','0:0:0:0:0:0:0:1','','','认证中心','null','null','yaml','',''), +(4,'storm-monitor-dev.yml','DEFAULT_GROUP','# spring\nspring:\n security:\n user:\n name: ruoyi\n password: 123456\n boot:\n admin:\n ui:\n title: 若依服务状态监控\n','6f122fd2bfb8d45f858e7d6529a9cd44','2020-11-20 00:00:00','2024-08-29 12:15:11','nacos','0:0:0:0:0:0:0:1','','','监控中心','null','null','yaml','',''), +(5,'storm-system-dev.yml','DEFAULT_GROUP','# spring配置\nspring:\n redis:\n host: 113.45.36.253\n port: 6379\n password: 123456\n datasource:\n druid:\n stat-view-servlet:\n enabled: true\n loginUsername: ruoyi\n loginPassword: 123456\n dynamic:\n druid:\n initial-size: 5\n min-idle: 5\n maxActive: 20\n maxWait: 60000\n connectTimeout: 30000\n socketTimeout: 60000\n timeBetweenEvictionRunsMillis: 60000\n minEvictableIdleTimeMillis: 300000\n validationQuery: SELECT 1 FROM DUAL\n testWhileIdle: true\n testOnBorrow: false\n testOnReturn: false\n poolPreparedStatements: true\n maxPoolPreparedStatementPerConnectionSize: 20\n filters: stat,slf4j\n connectionProperties: druid.stat.mergeSql\\=true;druid.stat.slowSqlMillis\\=5000\n datasource:\n # 主库数据源\n master:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://113.45.36.253:3306/fb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: efc14641a39359c0\n # 从库数据源\n # slave:\n # username: \n # password: 123456\n # url: \n # driver-class-name: \n\n# mybatis配置\nmybatis:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.system\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath:mapper/**/*.xml\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'系统模块接口文档\'\n # 描述\n description: \'系统模块接口描述\'\n # 作者信息\n contact:\n name: RuoYi\n url: https://ruoyi.vip\n','786c7daf4543411fc65c3e48dfb15243','2020-11-20 00:00:00','2024-09-02 12:14:33','nacos','0:0:0:0:0:0:0:1','','','系统模块','null','null','yaml','',''), +(6,'storm-gen-dev.yml','DEFAULT_GROUP','# spring配置\nspring:\n redis:\n host: 113.45.36.253\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://113.45.36.253:3306/fb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: efc14641a39359c0\n\n# mybatis配置\nmybatis:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.gen.domain\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath:mapper/**/*.xml\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'代码生成接口文档\'\n # 描述\n description: \'代码生成接口描述\'\n # 作者信息\n contact:\n name: RuoYi\n url: https://ruoyi.vip\n\n# 代码生成\ngen:\n # 作者\n author: ruoyi\n # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool\n packageName: com.storm.system\n # 自动去除表前缀,默认是false\n autoRemovePre: false\n # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)\n tablePrefix: sys_\n # 是否允许生成文件覆盖到本地(自定义路径),默认不允许\n allowOverwrite: false','43d807aa0a4accbb193b6dc7e38ac8a3','2020-11-20 00:00:00','2024-12-25 08:29:33','nacos','0:0:0:0:0:0:0:1','','','代码生成','null','null','yaml','',''), +(7,'storm-job-dev.yml','DEFAULT_GROUP','# spring配置\nspring:\n redis:\n host: 113.45.36.253\n port: 6379\n password: 123456\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://113.45.36.253:3306/fb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8\n username: root\n password: efc14641a39359c0\n\n# mybatis配置\nmybatis:\n # 搜索指定包别名\n typeAliasesPackage: com.storm.job.domain\n # 配置mapper的扫描,找到所有的mapper.xml映射文件\n mapperLocations: classpath:mapper/**/*.xml\n\n# springdoc配置\nspringdoc:\n gatewayUrl: http://localhost:8080/${spring.application.name}\n api-docs:\n # 是否开启接口文档\n enabled: true\n info:\n # 标题\n title: \'定时任务接口文档\'\n # 描述\n description: \'定时任务接口描述\'\n # 作者信息\n contact:\n name: RuoYi\n url: https://ruoyi.vip\n','f78483f845777335b9ed4a9f84758848','2020-11-20 00:00:00','2024-09-02 12:14:56','nacos','0:0:0:0:0:0:0:1','','','定时任务','null','null','yaml','',''), +(8,'storm-file-dev.yml','DEFAULT_GROUP','# 本地文件上传 \r\nfile:\r\n domain: http://127.0.0.1:9300\r\n path: D:/ruoyi/uploadPath\r\n prefix: /statics\r\n\r\n# FastDFS配置\r\nfdfs:\r\n domain: http://8.129.231.12\r\n soTimeout: 3000\r\n connectTimeout: 2000\r\n trackerList: 8.129.231.12:22122\r\n\r\n# Minio配置\r\nminio:\r\n url: http://8.129.231.12:9000\r\n accessKey: minioadmin\r\n secretKey: minioadmin\r\n bucketName: test','5382b93f3d8059d6068c0501fdd41195','2020-11-20 00:00:00','2020-12-21 21:01:59',NULL,'0:0:0:0:0:0:0:1','','','文件服务','null','null','yaml',NULL,''), +(9,'sentinel-storm-gateway','DEFAULT_GROUP','[\r\n {\r\n \"resource\": \"storm-auth\",\r\n \"count\": 500,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n },\r\n {\r\n \"resource\": \"storm-system\",\r\n \"count\": 1000,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n },\r\n {\r\n \"resource\": \"storm-gen\",\r\n \"count\": 200,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n },\r\n {\r\n \"resource\": \"storm-job\",\r\n \"count\": 300,\r\n \"grade\": 1,\r\n \"limitApp\": \"default\",\r\n \"strategy\": 0,\r\n \"controlBehavior\": 0\r\n }\r\n]','9f3a3069261598f74220bc47958ec252','2020-11-20 00:00:00','2020-11-20 00:00:00',NULL,'0:0:0:0:0:0:0:1','','','限流策略','null','null','json',NULL,''); + + +/******************************************/ +/* 表名称 = config_info_aggr */ +/******************************************/ +CREATE TABLE `config_info_aggr` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(255) NOT NULL COMMENT 'group_id', + `datum_id` varchar(255) NOT NULL COMMENT 'datum_id', + `content` longtext NOT NULL COMMENT '内容', + `gmt_modified` datetime NOT NULL COMMENT '修改时间', + `app_name` varchar(128) DEFAULT NULL, + `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段'; + + +/******************************************/ +/* 表名称 = config_info since 2.5.0 */ +/******************************************/ +CREATE TABLE `config_info_gray` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `src_user` text COMMENT 'src_user', + `src_ip` varchar(100) DEFAULT NULL COMMENT 'src_ip', + `gmt_create` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_create', + `gmt_modified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_modified', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', + `gray_name` varchar(128) NOT NULL COMMENT 'gray_name', + `gray_rule` text NOT NULL COMMENT 'gray_rule', + `encrypted_data_key` varchar(256) NOT NULL DEFAULT '' COMMENT 'encrypted_data_key', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfogray_datagrouptenantgray` (`data_id`,`group_id`,`tenant_id`,`gray_name`), + KEY `idx_dataid_gmt_modified` (`data_id`,`gmt_modified`), + KEY `idx_gmt_modified` (`gmt_modified`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='config_info_gray'; + + +/******************************************/ +/* 表名称 = config_info_beta */ +/******************************************/ +CREATE TABLE `config_info_beta` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `content` longtext NOT NULL COMMENT 'content', + `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COMMENT 'source user', + `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', + `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', + `encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta'; + +/******************************************/ +/* 表名称 = config_info_tag */ +/******************************************/ +CREATE TABLE `config_info_tag` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', + `tag_id` varchar(128) NOT NULL COMMENT 'tag_id', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COMMENT 'source user', + `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag'; + +/******************************************/ +/* 表名称 = config_tags_relation */ +/******************************************/ +CREATE TABLE `config_tags_relation` ( + `id` bigint(20) NOT NULL COMMENT 'id', + `tag_name` varchar(128) NOT NULL COMMENT 'tag_name', + `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', + `nid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识', + PRIMARY KEY (`nid`), + UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), + KEY `idx_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation'; + +/******************************************/ +/* 表名称 = group_capacity */ +/******************************************/ +CREATE TABLE `group_capacity` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群', + `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', + `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', + `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', + `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值', + `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', + `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_group_id` (`group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表'; + +/******************************************/ +/* 表名称 = his_config_info */ +/******************************************/ +CREATE TABLE `his_config_info` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'id', + `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COMMENT 'source user', + `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', + `op_type` char(10) DEFAULT NULL COMMENT 'operation type', + `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', + `encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥', + `publish_type` varchar(50) DEFAULT 'formal' COMMENT 'publish type gray or formal', + `gray_name` varchar(50) DEFAULT NULL COMMENT 'gray name', + `ext_info` longtext DEFAULT NULL COMMENT 'ext info', + PRIMARY KEY (`nid`), + KEY `idx_gmt_create` (`gmt_create`), + KEY `idx_gmt_modified` (`gmt_modified`), + KEY `idx_did` (`data_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造'; + + +/******************************************/ +/* 数据库全名 = nacos_config */ +/* 表名称 = tenant_capacity */ +/******************************************/ +CREATE TABLE `tenant_capacity` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID', + `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', + `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', + `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', + `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数', + `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', + `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表'; + + +CREATE TABLE `tenant_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `kp` varchar(128) NOT NULL COMMENT 'kp', + `tenant_id` varchar(128) default '' COMMENT 'tenant_id', + `tenant_name` varchar(128) default '' COMMENT 'tenant_name', + `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc', + `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source', + `gmt_create` bigint(20) NOT NULL COMMENT '创建时间', + `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), + KEY `idx_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info'; + +CREATE TABLE `users` ( + `username` varchar(50) NOT NULL PRIMARY KEY COMMENT 'username', + `password` varchar(500) NOT NULL COMMENT 'password', + `enabled` boolean NOT NULL COMMENT 'enabled' +); + +CREATE TABLE `roles` ( + `username` varchar(50) NOT NULL COMMENT 'username', + `role` varchar(50) NOT NULL COMMENT 'role', + UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE +); + +CREATE TABLE `permissions` ( + `role` varchar(50) NOT NULL COMMENT 'role', + `resource` varchar(128) NOT NULL COMMENT 'resource', + `action` varchar(8) NOT NULL COMMENT 'action', + UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE +); + +INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE); + +INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN'); diff --git a/sql/ry_seata_20210128.sql b/sql/ry_seata_20210128.sql new file mode 100644 index 0000000..279f3b8 --- /dev/null +++ b/sql/ry_seata_20210128.sql @@ -0,0 +1,80 @@ +DROP DATABASE IF EXISTS `storm-seata`; + +CREATE DATABASE `storm-seata` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +USE `storm-seata`; + +-- -------------------------------- The script used when storeMode is 'db' -------------------------------- +-- the table to store GlobalSession data +CREATE TABLE IF NOT EXISTS `global_table` +( + `xid` VARCHAR(128) NOT NULL, + `transaction_id` BIGINT, + `status` TINYINT NOT NULL, + `application_id` VARCHAR(32), + `transaction_service_group` VARCHAR(32), + `transaction_name` VARCHAR(128), + `timeout` INT, + `begin_time` BIGINT, + `application_data` VARCHAR(2000), + `gmt_create` DATETIME, + `gmt_modified` DATETIME, + PRIMARY KEY (`xid`), + KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), + KEY `idx_transaction_id` (`transaction_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +-- the table to store BranchSession data +CREATE TABLE IF NOT EXISTS `branch_table` +( + `branch_id` BIGINT NOT NULL, + `xid` VARCHAR(128) NOT NULL, + `transaction_id` BIGINT, + `resource_group_id` VARCHAR(32), + `resource_id` VARCHAR(256), + `branch_type` VARCHAR(8), + `status` TINYINT, + `client_id` VARCHAR(64), + `application_data` VARCHAR(2000), + `gmt_create` DATETIME(6), + `gmt_modified` DATETIME(6), + PRIMARY KEY (`branch_id`), + KEY `idx_xid` (`xid`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +-- the table to store lock data +CREATE TABLE IF NOT EXISTS `lock_table` +( + `row_key` VARCHAR(128) NOT NULL, + `xid` VARCHAR(96), + `transaction_id` BIGINT, + `branch_id` BIGINT NOT NULL, + `resource_id` VARCHAR(256), + `table_name` VARCHAR(32), + `pk` VARCHAR(36), + `gmt_create` DATETIME, + `gmt_modified` DATETIME, + PRIMARY KEY (`row_key`), + KEY `idx_branch_id` (`branch_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +-- for AT mode you must to init this sql for you business database. the seata server not need it. +CREATE TABLE IF NOT EXISTS `undo_log` +( + `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', + `xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id', + `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', + `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', + `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', + `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', + `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', + UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) +) ENGINE = InnoDB + AUTO_INCREMENT = 1 + DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table'; \ No newline at end of file diff --git a/storm-api/pom.xml b/storm-api/pom.xml new file mode 100644 index 0000000..cdbff36 --- /dev/null +++ b/storm-api/pom.xml @@ -0,0 +1,22 @@ + + + + com.storm + storm + 3.6.6 + + 4.0.0 + + + storm-api-system + + + storm-api + pom + + + storm-api系统接口 + + + diff --git a/storm-api/storm-api-system/pom.xml b/storm-api/storm-api-system/pom.xml new file mode 100644 index 0000000..e675caa --- /dev/null +++ b/storm-api/storm-api-system/pom.xml @@ -0,0 +1,28 @@ + + + + com.storm + storm-api + 3.6.6 + + 4.0.0 + + storm-api-system + + + storm-api-system系统接口模块 + + + + + + + com.storm + storm-common-core + + + + + \ No newline at end of file diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/RemoteFileService.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/RemoteFileService.java new file mode 100644 index 0000000..b5af222 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/RemoteFileService.java @@ -0,0 +1,40 @@ +package com.storm.system.api; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; +import com.storm.common.core.constant.ServiceNameConstants; +import com.storm.common.core.domain.R; +import com.storm.system.api.domain.SysFile; +import com.storm.system.api.factory.RemoteFileFallbackFactory; + +/** + * 文件服务 + * + * @author ruoyi + */ +@FeignClient(contextId = "remoteFileService", value = ServiceNameConstants.FILE_SERVICE, fallbackFactory = RemoteFileFallbackFactory.class) +public interface RemoteFileService +{ + /** + * 上传文件 + * + * @param file 文件信息 + * @return 结果 + */ + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R upload(@RequestPart(value = "file") MultipartFile file); + + /** + * 删除文件 + * + * @param fileUrl 文件地址 + * @return 结果 + */ + @DeleteMapping(value = "/delete", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public R delete(@RequestParam("fileUrl") String fileUrl); +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/RemoteLogService.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/RemoteLogService.java new file mode 100644 index 0000000..2d344db --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/RemoteLogService.java @@ -0,0 +1,41 @@ +package com.storm.system.api; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.constant.ServiceNameConstants; +import com.storm.common.core.domain.R; +import com.storm.system.api.domain.SysLogininfor; +import com.storm.system.api.domain.SysOperLog; +import com.storm.system.api.factory.RemoteLogFallbackFactory; + +/** + * 日志服务 + * + * @author ruoyi + */ +@FeignClient(contextId = "remoteLogService", value = ServiceNameConstants.SYSTEM_SERVICE, fallbackFactory = RemoteLogFallbackFactory.class) +public interface RemoteLogService +{ + /** + * 保存系统日志 + * + * @param sysOperLog 日志实体 + * @param source 请求来源 + * @return 结果 + */ + @PostMapping("/operlog") + public R saveLog(@RequestBody SysOperLog sysOperLog, @RequestHeader(SecurityConstants.FROM_SOURCE) String source) throws Exception; + + /** + * 保存访问记录 + * + * @param sysLogininfor 访问实体 + * @param source 请求来源 + * @return 结果 + */ + @PostMapping("/logininfor") + public R saveLogininfor(@RequestBody SysLogininfor sysLogininfor, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/RemoteUserService.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/RemoteUserService.java new file mode 100644 index 0000000..46fce24 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/RemoteUserService.java @@ -0,0 +1,54 @@ +package com.storm.system.api; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.constant.ServiceNameConstants; +import com.storm.common.core.domain.R; +import com.storm.system.api.domain.SysUser; +import com.storm.system.api.factory.RemoteUserFallbackFactory; +import com.storm.system.api.model.LoginUser; + +/** + * 用户服务 + * + * @author ruoyi + */ +@FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.SYSTEM_SERVICE, fallbackFactory = RemoteUserFallbackFactory.class) +public interface RemoteUserService +{ + /** + * 通过用户名查询用户信息 + * + * @param username 用户名 + * @param source 请求来源 + * @return 结果 + */ + @GetMapping("/user/info/{username}") + public R getUserInfo(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); + + /** + * 注册用户信息 + * + * @param sysUser 用户信息 + * @param source 请求来源 + * @return 结果 + */ + @PostMapping("/user/register") + public R registerUserInfo(@RequestBody SysUser sysUser, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); + + /** + * 记录用户登录IP地址和登录时间 + * + * @param sysUser 用户信息 + * @param source 请求来源 + * @return 结果 + */ + @PutMapping("/user/recordlogin") + public R recordUserLogin(@RequestBody SysUser sysUser, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysDept.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysDept.java new file mode 100644 index 0000000..23b9486 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysDept.java @@ -0,0 +1,203 @@ +package com.storm.system.api.domain; + +import java.util.ArrayList; +import java.util.List; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 部门表 sys_dept + * + * @author ruoyi + */ +public class SysDept extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 部门ID */ + private Long deptId; + + /** 父部门ID */ + private Long parentId; + + /** 祖级列表 */ + private String ancestors; + + /** 部门名称 */ + private String deptName; + + /** 显示顺序 */ + private Integer orderNum; + + /** 负责人 */ + private String leader; + + /** 联系电话 */ + private String phone; + + /** 邮箱 */ + private String email; + + /** 部门状态:0正常,1停用 */ + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 父部门名称 */ + private String parentName; + + /** 子部门 */ + private List children = new ArrayList(); + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + public String getAncestors() + { + return ancestors; + } + + public void setAncestors(String ancestors) + { + this.ancestors = ancestors; + } + + @NotBlank(message = "部门名称不能为空") + @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符") + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + public String getLeader() + { + return leader; + } + + public void setLeader(String leader) + { + this.leader = leader; + } + + @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符") + public String getPhone() + { + return phone; + } + + public void setPhone(String phone) + { + this.phone = phone; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("deptId", getDeptId()) + .append("parentId", getParentId()) + .append("ancestors", getAncestors()) + .append("deptName", getDeptName()) + .append("orderNum", getOrderNum()) + .append("leader", getLeader()) + .append("phone", getPhone()) + .append("email", getEmail()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysDictData.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysDictData.java new file mode 100644 index 0000000..6e95a4c --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysDictData.java @@ -0,0 +1,176 @@ +package com.storm.system.api.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.annotation.Excel.ColumnType; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 字典数据表 sys_dict_data + * + * @author ruoyi + */ +public class SysDictData extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 字典编码 */ + @Excel(name = "字典编码", cellType = ColumnType.NUMERIC) + private Long dictCode; + + /** 字典排序 */ + @Excel(name = "字典排序", cellType = ColumnType.NUMERIC) + private Long dictSort; + + /** 字典标签 */ + @Excel(name = "字典标签") + private String dictLabel; + + /** 字典键值 */ + @Excel(name = "字典键值") + private String dictValue; + + /** 字典类型 */ + @Excel(name = "字典类型") + private String dictType; + + /** 样式属性(其他样式扩展) */ + private String cssClass; + + /** 表格字典样式 */ + private String listClass; + + /** 是否默认(Y是 N否) */ + @Excel(name = "是否默认", readConverterExp = "Y=是,N=否") + private String isDefault; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictCode() + { + return dictCode; + } + + public void setDictCode(Long dictCode) + { + this.dictCode = dictCode; + } + + public Long getDictSort() + { + return dictSort; + } + + public void setDictSort(Long dictSort) + { + this.dictSort = dictSort; + } + + @NotBlank(message = "字典标签不能为空") + @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符") + public String getDictLabel() + { + return dictLabel; + } + + public void setDictLabel(String dictLabel) + { + this.dictLabel = dictLabel; + } + + @NotBlank(message = "字典键值不能为空") + @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符") + public String getDictValue() + { + return dictValue; + } + + public void setDictValue(String dictValue) + { + this.dictValue = dictValue; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符") + public String getDictType() + { + return dictType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符") + public String getCssClass() + { + return cssClass; + } + + public void setCssClass(String cssClass) + { + this.cssClass = cssClass; + } + + public String getListClass() + { + return listClass; + } + + public void setListClass(String listClass) + { + this.listClass = listClass; + } + + public boolean getDefault() + { + return UserConstants.YES.equals(this.isDefault); + } + + public String getIsDefault() + { + return isDefault; + } + + public void setIsDefault(String isDefault) + { + this.isDefault = isDefault; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("dictCode", getDictCode()) + .append("dictSort", getDictSort()) + .append("dictLabel", getDictLabel()) + .append("dictValue", getDictValue()) + .append("dictType", getDictType()) + .append("cssClass", getCssClass()) + .append("listClass", getListClass()) + .append("isDefault", getIsDefault()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysDictType.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysDictType.java new file mode 100644 index 0000000..9fc8864 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysDictType.java @@ -0,0 +1,96 @@ +package com.storm.system.api.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.annotation.Excel.ColumnType; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 字典类型表 sys_dict_type + * + * @author ruoyi + */ +public class SysDictType extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 字典主键 */ + @Excel(name = "字典主键", cellType = ColumnType.NUMERIC) + private Long dictId; + + /** 字典名称 */ + @Excel(name = "字典名称") + private String dictName; + + /** 字典类型 */ + @Excel(name = "字典类型") + private String dictType; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictId() + { + return dictId; + } + + public void setDictId(Long dictId) + { + this.dictId = dictId; + } + + @NotBlank(message = "字典名称不能为空") + @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符") + public String getDictName() + { + return dictName; + } + + public void setDictName(String dictName) + { + this.dictName = dictName; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符") + @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)") + public String getDictType() + { + return dictType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("dictId", getDictId()) + .append("dictName", getDictName()) + .append("dictType", getDictType()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysFile.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysFile.java new file mode 100644 index 0000000..0798b10 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysFile.java @@ -0,0 +1,50 @@ +package com.storm.system.api.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 文件信息 + * + * @author ruoyi + */ +public class SysFile +{ + /** + * 文件名称 + */ + private String name; + + /** + * 文件地址 + */ + private String url; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getUrl() + { + return url; + } + + public void setUrl(String url) + { + this.url = url; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("name", getName()) + .append("url", getUrl()) + .toString(); + } +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysLogininfor.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysLogininfor.java new file mode 100644 index 0000000..c7ed8f9 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysLogininfor.java @@ -0,0 +1,102 @@ +package com.storm.system.api.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.annotation.Excel.ColumnType; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 系统访问记录表 sys_logininfor + * + * @author ruoyi + */ +public class SysLogininfor extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "序号", cellType = ColumnType.NUMERIC) + private Long infoId; + + /** 用户账号 */ + @Excel(name = "用户账号") + private String userName; + + /** 状态 0成功 1失败 */ + @Excel(name = "状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** 地址 */ + @Excel(name = "地址") + private String ipaddr; + + /** 描述 */ + @Excel(name = "描述") + private String msg; + + /** 访问时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date accessTime; + + public Long getInfoId() + { + return infoId; + } + + public void setInfoId(Long infoId) + { + this.infoId = infoId; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public Date getAccessTime() + { + return accessTime; + } + + public void setAccessTime(Date accessTime) + { + this.accessTime = accessTime; + } +} \ No newline at end of file diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysOperLog.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysOperLog.java new file mode 100644 index 0000000..decf367 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysOperLog.java @@ -0,0 +1,255 @@ +package com.storm.system.api.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.annotation.Excel.ColumnType; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 操作日志记录表 oper_log + * + * @author ruoyi + */ +public class SysOperLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 日志主键 */ + @Excel(name = "操作序号", cellType = ColumnType.NUMERIC) + private Long operId; + + /** 操作模块 */ + @Excel(name = "操作模块") + private String title; + + /** 业务类型(0其它 1新增 2修改 3删除) */ + @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据") + private Integer businessType; + + /** 业务类型数组 */ + private Integer[] businessTypes; + + /** 请求方法 */ + @Excel(name = "请求方法") + private String method; + + /** 请求方式 */ + @Excel(name = "请求方式") + private String requestMethod; + + /** 操作类别(0其它 1后台用户 2手机端用户) */ + @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户") + private Integer operatorType; + + /** 操作人员 */ + @Excel(name = "操作人员") + private String operName; + + /** 部门名称 */ + @Excel(name = "部门名称") + private String deptName; + + /** 请求url */ + @Excel(name = "请求地址") + private String operUrl; + + /** 操作地址 */ + @Excel(name = "操作地址") + private String operIp; + + /** 请求参数 */ + @Excel(name = "请求参数") + private String operParam; + + /** 返回参数 */ + @Excel(name = "返回参数") + private String jsonResult; + + /** 操作状态(0正常 1异常) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=异常") + private Integer status; + + /** 错误消息 */ + @Excel(name = "错误消息") + private String errorMsg; + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date operTime; + + /** 消耗时间 */ + @Excel(name = "消耗时间", suffix = "毫秒") + private Long costTime; + + public Long getOperId() + { + return operId; + } + + public void setOperId(Long operId) + { + this.operId = operId; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public Integer getBusinessType() + { + return businessType; + } + + public void setBusinessType(Integer businessType) + { + this.businessType = businessType; + } + + public Integer[] getBusinessTypes() + { + return businessTypes; + } + + public void setBusinessTypes(Integer[] businessTypes) + { + this.businessTypes = businessTypes; + } + + public String getMethod() + { + return method; + } + + public void setMethod(String method) + { + this.method = method; + } + + public String getRequestMethod() + { + return requestMethod; + } + + public void setRequestMethod(String requestMethod) + { + this.requestMethod = requestMethod; + } + + public Integer getOperatorType() + { + return operatorType; + } + + public void setOperatorType(Integer operatorType) + { + this.operatorType = operatorType; + } + + public String getOperName() + { + return operName; + } + + public void setOperName(String operName) + { + this.operName = operName; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getOperUrl() + { + return operUrl; + } + + public void setOperUrl(String operUrl) + { + this.operUrl = operUrl; + } + + public String getOperIp() + { + return operIp; + } + + public void setOperIp(String operIp) + { + this.operIp = operIp; + } + + public String getOperParam() + { + return operParam; + } + + public void setOperParam(String operParam) + { + this.operParam = operParam; + } + + public String getJsonResult() + { + return jsonResult; + } + + public void setJsonResult(String jsonResult) + { + this.jsonResult = jsonResult; + } + + public Integer getStatus() + { + return status; + } + + public void setStatus(Integer status) + { + this.status = status; + } + + public String getErrorMsg() + { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) + { + this.errorMsg = errorMsg; + } + + public Date getOperTime() + { + return operTime; + } + + public void setOperTime(Date operTime) + { + this.operTime = operTime; + } + + public Long getCostTime() + { + return costTime; + } + + public void setCostTime(Long costTime) + { + this.costTime = costTime; + } +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysRole.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysRole.java new file mode 100644 index 0000000..ca01d35 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysRole.java @@ -0,0 +1,241 @@ +package com.storm.system.api.domain; + +import java.util.Set; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.annotation.Excel.ColumnType; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 角色表 sys_role + * + * @author ruoyi + */ +public class SysRole extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 角色ID */ + @Excel(name = "角色序号", cellType = ColumnType.NUMERIC) + private Long roleId; + + /** 角色名称 */ + @Excel(name = "角色名称") + private String roleName; + + /** 角色权限 */ + @Excel(name = "角色权限") + private String roleKey; + + /** 角色排序 */ + @Excel(name = "角色排序") + private Integer roleSort; + + /** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) */ + @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限") + private String dataScope; + + /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */ + private boolean menuCheckStrictly; + + /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */ + private boolean deptCheckStrictly; + + /** 角色状态(0正常 1停用) */ + @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 用户是否存在此角色标识 默认不存在 */ + private boolean flag = false; + + /** 菜单组 */ + private Long[] menuIds; + + /** 部门组(数据权限) */ + private Long[] deptIds; + + /** 角色菜单权限 */ + private Set permissions; + + public SysRole() + { + + } + + public SysRole(Long roleId) + { + this.roleId = roleId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public boolean isAdmin() + { + return isAdmin(this.roleId); + } + + public static boolean isAdmin(Long roleId) + { + return roleId != null && 1L == roleId; + } + + @NotBlank(message = "角色名称不能为空") + @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符") + public String getRoleName() + { + return roleName; + } + + public void setRoleName(String roleName) + { + this.roleName = roleName; + } + + @NotBlank(message = "权限字符不能为空") + @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符") + public String getRoleKey() + { + return roleKey; + } + + public void setRoleKey(String roleKey) + { + this.roleKey = roleKey; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getRoleSort() + { + return roleSort; + } + + public void setRoleSort(Integer roleSort) + { + this.roleSort = roleSort; + } + + public String getDataScope() + { + return dataScope; + } + + public void setDataScope(String dataScope) + { + this.dataScope = dataScope; + } + + public boolean isMenuCheckStrictly() + { + return menuCheckStrictly; + } + + public void setMenuCheckStrictly(boolean menuCheckStrictly) + { + this.menuCheckStrictly = menuCheckStrictly; + } + + public boolean isDeptCheckStrictly() + { + return deptCheckStrictly; + } + + public void setDeptCheckStrictly(boolean deptCheckStrictly) + { + this.deptCheckStrictly = deptCheckStrictly; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public boolean isFlag() + { + return flag; + } + + public void setFlag(boolean flag) + { + this.flag = flag; + } + + public Long[] getMenuIds() + { + return menuIds; + } + + public void setMenuIds(Long[] menuIds) + { + this.menuIds = menuIds; + } + + public Long[] getDeptIds() + { + return deptIds; + } + + public void setDeptIds(Long[] deptIds) + { + this.deptIds = deptIds; + } + + public Set getPermissions() + { + return permissions; + } + + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("roleName", getRoleName()) + .append("roleKey", getRoleKey()) + .append("roleSort", getRoleSort()) + .append("dataScope", getDataScope()) + .append("menuCheckStrictly", isMenuCheckStrictly()) + .append("deptCheckStrictly", isDeptCheckStrictly()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysUser.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysUser.java new file mode 100644 index 0000000..784c601 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/domain/SysUser.java @@ -0,0 +1,339 @@ +package com.storm.system.api.domain; + +import java.util.Date; +import java.util.List; +import javax.validation.constraints.*; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.annotation.Excel.ColumnType; +import com.storm.common.core.annotation.Excel.Type; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.annotation.Excels; +import com.storm.common.core.web.domain.BaseEntity; +import com.storm.common.core.xss.Xss; + +/** + * 用户对象 sys_user + * + * @author ruoyi + */ +public class SysUser extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 用户ID */ + @Excel(name = "用户序号", type = Type.EXPORT, cellType = ColumnType.NUMERIC, prompt = "用户编号") + private Long userId; + + /** 部门ID */ + @Excel(name = "部门编号", type = Type.IMPORT) + private Long deptId; + + /** 用户账号 */ + @Excel(name = "登录名称") + private String userName; + + /** 用户昵称 */ + @Excel(name = "用户名称") + private String nickName; + + /** 用户邮箱 */ + @Excel(name = "用户邮箱") + private String email; + + /** 手机号码 */ + @Excel(name = "手机号码", cellType = ColumnType.TEXT) + private String phonenumber; + + /** 用户性别 */ + @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知") + private String sex; + + /** 用户头像 */ + private String avatar; + + /** 密码 */ + private String password; + + /** 账号状态(0正常 1停用) */ + @Excel(name = "账号状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 最后登录IP */ + @Excel(name = "最后登录IP", type = Type.EXPORT) + private String loginIp; + + /** 最后登录时间 */ + @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT) + private Date loginDate; + + /** 密码最后更新时间 */ + private Date pwdUpdateDate; + + /** 部门对象 */ + @Excels({ + @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT), + @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT) + }) + private SysDept dept; + + /** 角色对象 */ + private List roles; + + /** 角色组 */ + private Long[] roleIds; + + /** 岗位组 */ + private Long[] postIds; + + /** 角色ID */ + private Long roleId; + + public SysUser() + { + + } + + public SysUser(Long userId) + { + this.userId = userId; + } + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public boolean isAdmin() + { + return isAdmin(this.userId); + } + + public static boolean isAdmin(Long userId) + { + return UserConstants.isAdmin(userId); + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + @Xss(message = "用户昵称不能包含脚本字符") + @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") + public String getNickName() + { + return nickName; + } + + public void setNickName(String nickName) + { + this.nickName = nickName; + } + + @Xss(message = "用户账号不能包含脚本字符") + @NotBlank(message = "用户账号不能为空") + @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } + + @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符") + public String getPhonenumber() + { + return phonenumber; + } + + public void setPhonenumber(String phonenumber) + { + this.phonenumber = phonenumber; + } + + public String getSex() + { + return sex; + } + + public void setSex(String sex) + { + this.sex = sex; + } + + public String getAvatar() + { + return avatar; + } + + public void setAvatar(String avatar) + { + this.avatar = avatar; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getLoginIp() + { + return loginIp; + } + + public void setLoginIp(String loginIp) + { + this.loginIp = loginIp; + } + + public Date getLoginDate() + { + return loginDate; + } + + public void setLoginDate(Date loginDate) + { + this.loginDate = loginDate; + } + + public Date getPwdUpdateDate() + { + return pwdUpdateDate; + } + + public void setPwdUpdateDate(Date pwdUpdateDate) + { + this.pwdUpdateDate = pwdUpdateDate; + } + + public SysDept getDept() + { + return dept; + } + + public void setDept(SysDept dept) + { + this.dept = dept; + } + + public List getRoles() + { + return roles; + } + + public void setRoles(List roles) + { + this.roles = roles; + } + + public Long[] getRoleIds() + { + return roleIds; + } + + public void setRoleIds(Long[] roleIds) + { + this.roleIds = roleIds; + } + + public Long[] getPostIds() + { + return postIds; + } + + public void setPostIds(Long[] postIds) + { + this.postIds = postIds; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("deptId", getDeptId()) + .append("userName", getUserName()) + .append("nickName", getNickName()) + .append("email", getEmail()) + .append("phonenumber", getPhonenumber()) + .append("sex", getSex()) + .append("avatar", getAvatar()) + .append("password", getPassword()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("loginIp", getLoginIp()) + .append("loginDate", getLoginDate()) + .append("pwdUpdateDate", getPwdUpdateDate()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .append("dept", getDept()) + .toString(); + } +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/factory/RemoteFileFallbackFactory.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/factory/RemoteFileFallbackFactory.java new file mode 100644 index 0000000..200e1bf --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/factory/RemoteFileFallbackFactory.java @@ -0,0 +1,41 @@ +package com.storm.system.api.factory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import com.storm.common.core.domain.R; +import com.storm.system.api.RemoteFileService; +import com.storm.system.api.domain.SysFile; + +/** + * 文件服务降级处理 + * + * @author ruoyi + */ +@Component +public class RemoteFileFallbackFactory implements FallbackFactory +{ + private static final Logger log = LoggerFactory.getLogger(RemoteFileFallbackFactory.class); + + @Override + public RemoteFileService create(Throwable throwable) + { + log.error("文件服务调用失败:{}", throwable.getMessage()); + return new RemoteFileService() + { + @Override + public R upload(MultipartFile file) + { + return R.fail("上传文件失败:" + throwable.getMessage()); + } + + @Override + public R delete(String fileUrl) + { + return R.fail("删除文件失败:" + throwable.getMessage()); + } + }; + } +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/factory/RemoteLogFallbackFactory.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/factory/RemoteLogFallbackFactory.java new file mode 100644 index 0000000..a28f4c6 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/factory/RemoteLogFallbackFactory.java @@ -0,0 +1,42 @@ +package com.storm.system.api.factory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; +import com.storm.common.core.domain.R; +import com.storm.system.api.RemoteLogService; +import com.storm.system.api.domain.SysLogininfor; +import com.storm.system.api.domain.SysOperLog; + +/** + * 日志服务降级处理 + * + * @author ruoyi + */ +@Component +public class RemoteLogFallbackFactory implements FallbackFactory +{ + private static final Logger log = LoggerFactory.getLogger(RemoteLogFallbackFactory.class); + + @Override + public RemoteLogService create(Throwable throwable) + { + log.error("日志服务调用失败:{}", throwable.getMessage()); + return new RemoteLogService() + { + @Override + public R saveLog(SysOperLog sysOperLog, String source) + { + return R.fail("保存操作日志失败:" + throwable.getMessage()); + } + + @Override + public R saveLogininfor(SysLogininfor sysLogininfor, String source) + { + return R.fail("保存登录日志失败:" + throwable.getMessage()); + } + }; + + } +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/factory/RemoteUserFallbackFactory.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/factory/RemoteUserFallbackFactory.java new file mode 100644 index 0000000..b99442e --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/factory/RemoteUserFallbackFactory.java @@ -0,0 +1,47 @@ +package com.storm.system.api.factory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; +import com.storm.common.core.domain.R; +import com.storm.system.api.RemoteUserService; +import com.storm.system.api.domain.SysUser; +import com.storm.system.api.model.LoginUser; + +/** + * 用户服务降级处理 + * + * @author ruoyi + */ +@Component +public class RemoteUserFallbackFactory implements FallbackFactory +{ + private static final Logger log = LoggerFactory.getLogger(RemoteUserFallbackFactory.class); + + @Override + public RemoteUserService create(Throwable throwable) + { + log.error("用户服务调用失败:{}", throwable.getMessage()); + return new RemoteUserService() + { + @Override + public R getUserInfo(String username, String source) + { + return R.fail("获取用户失败:" + throwable.getMessage()); + } + + @Override + public R registerUserInfo(SysUser sysUser, String source) + { + return R.fail("注册用户失败:" + throwable.getMessage()); + } + + @Override + public R recordUserLogin(SysUser sysUser, String source) + { + return R.fail("记录用户登录信息失败:" + throwable.getMessage()); + } + }; + } +} diff --git a/storm-api/storm-api-system/src/main/java/com/storm/system/api/model/LoginUser.java b/storm-api/storm-api-system/src/main/java/com/storm/system/api/model/LoginUser.java new file mode 100644 index 0000000..555f125 --- /dev/null +++ b/storm-api/storm-api-system/src/main/java/com/storm/system/api/model/LoginUser.java @@ -0,0 +1,150 @@ +package com.storm.system.api.model; + +import java.io.Serializable; +import java.util.Set; +import com.storm.system.api.domain.SysUser; + +/** + * 用户信息 + * + * @author ruoyi + */ +public class LoginUser implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 用户名id + */ + private Long userid; + + /** + * 用户名 + */ + private String username; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 角色列表 + */ + private Set roles; + + /** + * 用户信息 + */ + private SysUser sysUser; + + public String getToken() + { + return token; + } + + public void setToken(String token) + { + this.token = token; + } + + public Long getUserid() + { + return userid; + } + + public void setUserid(Long userid) + { + this.userid = userid; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } + + public Long getExpireTime() + { + return expireTime; + } + + public void setExpireTime(Long expireTime) + { + this.expireTime = expireTime; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public Set getPermissions() + { + return permissions; + } + + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + public Set getRoles() + { + return roles; + } + + public void setRoles(Set roles) + { + this.roles = roles; + } + + public SysUser getSysUser() + { + return sysUser; + } + + public void setSysUser(SysUser sysUser) + { + this.sysUser = sysUser; + } +} diff --git a/storm-api/storm-api-system/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/storm-api/storm-api-system/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..9c0ddac --- /dev/null +++ b/storm-api/storm-api-system/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +com.storm.system.api.factory.RemoteUserFallbackFactory +com.storm.system.api.factory.RemoteLogFallbackFactory +com.storm.system.api.factory.RemoteFileFallbackFactory diff --git a/storm-auth/pom.xml b/storm-auth/pom.xml new file mode 100644 index 0000000..959644b --- /dev/null +++ b/storm-auth/pom.xml @@ -0,0 +1,79 @@ + + + com.storm + storm + 3.6.6 + + 4.0.0 + + storm-auth + + + storm-auth认证授权中心 + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + com.storm + storm-common-security + + + + org.projectlombok + lombok + + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + diff --git a/storm-auth/src/main/java/com/storm/auth/AuthApplication.java b/storm-auth/src/main/java/com/storm/auth/AuthApplication.java new file mode 100644 index 0000000..4790a2a --- /dev/null +++ b/storm-auth/src/main/java/com/storm/auth/AuthApplication.java @@ -0,0 +1,46 @@ +package com.storm.auth; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import com.storm.common.security.annotation.EnableRyFeignClients; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.env.Environment; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 认证授权中心 + * + * @author DengAo + */ +@Slf4j +@EnableRyFeignClients +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class }) +public class AuthApplication +{ + public static void main(String[] args) throws UnknownHostException { + SpringApplication app = new SpringApplicationBuilder(AuthApplication.class).build(args); + Environment env = app.run(args).getEnvironment(); + String protocol = "http"; + if (env.getProperty("server.ssl.key-store") != null) { + protocol = "https"; + } + log.info("--/\n---------------------------------------------------------------------------------------\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\t{}://localhost:{}\n\t" + + "External: \t{}://{}:{}\n\t" + + "Profile(s): \t{}" + + "\n---------------------------------------------------------------------------------------", + env.getProperty("spring.application.name"), + protocol, + env.getProperty("server.port"), + protocol, + InetAddress.getLocalHost().getHostAddress(), + env.getProperty("server.port"), + env.getActiveProfiles()); + log.info("--/\n------------------------------------------------------------------------------\n\t" ); + } +} diff --git a/storm-auth/src/main/java/com/storm/auth/controller/TokenController.java b/storm-auth/src/main/java/com/storm/auth/controller/TokenController.java new file mode 100644 index 0000000..5c0a124 --- /dev/null +++ b/storm-auth/src/main/java/com/storm/auth/controller/TokenController.java @@ -0,0 +1,78 @@ +package com.storm.auth.controller; + +import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import com.storm.auth.form.LoginBody; +import com.storm.auth.form.RegisterBody; +import com.storm.auth.service.SysLoginService; +import com.storm.common.core.domain.R; +import com.storm.common.core.utils.JwtUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.security.auth.AuthUtil; +import com.storm.common.security.service.TokenService; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.model.LoginUser; + +/** + * token 控制 + * + * @author ruoyi + */ +@RestController +public class TokenController +{ + @Autowired + private TokenService tokenService; + + @Autowired + private SysLoginService sysLoginService; + + @PostMapping("login") + public R login(@RequestBody LoginBody form) + { + // 用户登录 + LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword()); + // 获取登录token + return R.ok(tokenService.createToken(userInfo)); + } + + @DeleteMapping("logout") + public R logout(HttpServletRequest request) + { + String token = SecurityUtils.getToken(request); + if (StringUtils.isNotEmpty(token)) + { + String username = JwtUtils.getUserName(token); + // 删除用户缓存记录 + AuthUtil.logoutByToken(token); + // 记录用户退出日志 + sysLoginService.logout(username); + } + return R.ok(); + } + + @PostMapping("refresh") + public R refresh(HttpServletRequest request) + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser)) + { + // 刷新令牌有效期 + tokenService.refreshToken(loginUser); + return R.ok(); + } + return R.ok(); + } + + @PostMapping("register") + public R register(@RequestBody RegisterBody registerBody) + { + // 用户注册 + sysLoginService.register(registerBody.getUsername(), registerBody.getPassword()); + return R.ok(); + } +} diff --git a/storm-auth/src/main/java/com/storm/auth/form/LoginBody.java b/storm-auth/src/main/java/com/storm/auth/form/LoginBody.java new file mode 100644 index 0000000..a30c911 --- /dev/null +++ b/storm-auth/src/main/java/com/storm/auth/form/LoginBody.java @@ -0,0 +1,39 @@ +package com.storm.auth.form; + +/** + * 用户登录对象 + * + * @author ruoyi + */ +public class LoginBody +{ + /** + * 用户名 + */ + private String username; + + /** + * 用户密码 + */ + private String password; + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } +} diff --git a/storm-auth/src/main/java/com/storm/auth/form/RegisterBody.java b/storm-auth/src/main/java/com/storm/auth/form/RegisterBody.java new file mode 100644 index 0000000..5f3952a --- /dev/null +++ b/storm-auth/src/main/java/com/storm/auth/form/RegisterBody.java @@ -0,0 +1,11 @@ +package com.storm.auth.form; + +/** + * 用户注册对象 + * + * @author ruoyi + */ +public class RegisterBody extends LoginBody +{ + +} diff --git a/storm-auth/src/main/java/com/storm/auth/service/SysLoginService.java b/storm-auth/src/main/java/com/storm/auth/service/SysLoginService.java new file mode 100644 index 0000000..2cdc91f --- /dev/null +++ b/storm-auth/src/main/java/com/storm/auth/service/SysLoginService.java @@ -0,0 +1,156 @@ +package com.storm.auth.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.storm.common.core.constant.CacheConstants; +import com.storm.common.core.constant.Constants; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.domain.R; +import com.storm.common.core.enums.UserStatus; +import com.storm.common.core.exception.ServiceException; +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.DateUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.ip.IpUtils; +import com.storm.common.redis.service.RedisService; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.RemoteUserService; +import com.storm.system.api.domain.SysUser; +import com.storm.system.api.model.LoginUser; + +/** + * 登录校验方法 + * + * @author ruoyi + */ +@Component +public class SysLoginService +{ + @Autowired + private RemoteUserService remoteUserService; + + @Autowired + private SysPasswordService passwordService; + + @Autowired + private SysRecordLogService recordLogService; + + @Autowired + private RedisService redisService; + + /** + * 登录 + */ + public LoginUser login(String username, String password) + { + // 用户名或密码为空 错误 + if (StringUtils.isAnyBlank(username, password)) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写"); + throw new ServiceException("用户/密码必须填写"); + } + // 密码如果不在指定范围内 错误 + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围"); + throw new ServiceException("用户密码不在指定范围"); + } + // 用户名不在指定范围内 错误 + if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围"); + throw new ServiceException("用户名不在指定范围"); + } + // IP黑名单校验 + String blackStr = Convert.toStr(redisService.getCacheObject(CacheConstants.SYS_LOGIN_BLACKIPLIST)); + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "很遗憾,访问IP已被列入系统黑名单"); + throw new ServiceException("很遗憾,访问IP已被列入系统黑名单"); + } + // 查询用户信息 + R userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER); + + if (R.FAIL == userResult.getCode()) + { + throw new ServiceException(userResult.getMsg()); + } + + LoginUser userInfo = userResult.getData(); + SysUser user = userResult.getData().getSysUser(); + if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除"); + throw new ServiceException("对不起,您的账号:" + username + " 已被删除"); + } + if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员"); + throw new ServiceException("对不起,您的账号:" + username + " 已停用"); + } + passwordService.validate(user, password); + recordLogService.recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功"); + recordLoginInfo(user.getUserId()); + return userInfo; + } + + /** + * 记录登录信息 + * + * @param userId 用户ID + */ + public void recordLoginInfo(Long userId) + { + SysUser sysUser = new SysUser(); + sysUser.setUserId(userId); + // 更新用户登录IP + sysUser.setLoginIp(IpUtils.getIpAddr()); + // 更新用户登录时间 + sysUser.setLoginDate(DateUtils.getNowDate()); + remoteUserService.recordUserLogin(sysUser, SecurityConstants.INNER); + } + + public void logout(String loginName) + { + recordLogService.recordLogininfor(loginName, Constants.LOGOUT, "退出成功"); + } + + /** + * 注册 + */ + public void register(String username, String password) + { + // 用户名或密码为空 错误 + if (StringUtils.isAnyBlank(username, password)) + { + throw new ServiceException("用户/密码必须填写"); + } + if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + throw new ServiceException("账户长度必须在2到20个字符之间"); + } + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + throw new ServiceException("密码长度必须在5到20个字符之间"); + } + + // 注册用户信息 + SysUser sysUser = new SysUser(); + sysUser.setUserName(username); + sysUser.setNickName(username); + sysUser.setPwdUpdateDate(DateUtils.getNowDate()); + sysUser.setPassword(SecurityUtils.encryptPassword(password)); + R registerResult = remoteUserService.registerUserInfo(sysUser, SecurityConstants.INNER); + + if (R.FAIL == registerResult.getCode()) + { + throw new ServiceException(registerResult.getMsg()); + } + recordLogService.recordLogininfor(username, Constants.REGISTER, "注册成功"); + } +} diff --git a/storm-auth/src/main/java/com/storm/auth/service/SysPasswordService.java b/storm-auth/src/main/java/com/storm/auth/service/SysPasswordService.java new file mode 100644 index 0000000..6c32c07 --- /dev/null +++ b/storm-auth/src/main/java/com/storm/auth/service/SysPasswordService.java @@ -0,0 +1,85 @@ +package com.storm.auth.service; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.storm.common.core.constant.CacheConstants; +import com.storm.common.core.constant.Constants; +import com.storm.common.core.exception.ServiceException; +import com.storm.common.redis.service.RedisService; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysUser; + +/** + * 登录密码方法 + * + * @author ruoyi + */ +@Component +public class SysPasswordService +{ + @Autowired + private RedisService redisService; + + private int maxRetryCount = CacheConstants.PASSWORD_MAX_RETRY_COUNT; + + private Long lockTime = CacheConstants.PASSWORD_LOCK_TIME; + + @Autowired + private SysRecordLogService recordLogService; + + /** + * 登录账户密码错误次数缓存键名 + * + * @param username 用户名 + * @return 缓存键key + */ + private String getCacheKey(String username) + { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + public void validate(SysUser user, String password) + { + String username = user.getUserName(); + + Integer retryCount = redisService.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + String errMsg = String.format("密码输入错误%s次,帐户锁定%s分钟", maxRetryCount, lockTime); + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL,errMsg); + throw new ServiceException(errMsg); + } + + if (!matches(user, password)) + { + retryCount = retryCount + 1; + recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, String.format("密码输入错误%s次", retryCount)); + redisService.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new ServiceException("用户不存在/密码错误"); + } + else + { + clearLoginRecordCache(username); + } + } + + public boolean matches(SysUser user, String rawPassword) + { + return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); + } + + public void clearLoginRecordCache(String loginName) + { + if (redisService.hasKey(getCacheKey(loginName))) + { + redisService.deleteObject(getCacheKey(loginName)); + } + } +} diff --git a/storm-auth/src/main/java/com/storm/auth/service/SysRecordLogService.java b/storm-auth/src/main/java/com/storm/auth/service/SysRecordLogService.java new file mode 100644 index 0000000..28bfa6e --- /dev/null +++ b/storm-auth/src/main/java/com/storm/auth/service/SysRecordLogService.java @@ -0,0 +1,48 @@ +package com.storm.auth.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.storm.common.core.constant.Constants; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.ip.IpUtils; +import com.storm.system.api.RemoteLogService; +import com.storm.system.api.domain.SysLogininfor; + +/** + * 记录日志方法 + * + * @author ruoyi + */ +@Component +public class SysRecordLogService +{ + @Autowired + private RemoteLogService remoteLogService; + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息内容 + * @return + */ + public void recordLogininfor(String username, String status, String message) + { + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(IpUtils.getIpAddr()); + logininfor.setMsg(message); + // 日志状态 + if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) + { + logininfor.setStatus(Constants.LOGIN_SUCCESS_STATUS); + } + else if (Constants.LOGIN_FAIL.equals(status)) + { + logininfor.setStatus(Constants.LOGIN_FAIL_STATUS); + } + remoteLogService.saveLogininfor(logininfor, SecurityConstants.INNER); + } +} diff --git a/storm-auth/src/main/resources/banner.txt b/storm-auth/src/main/resources/banner.txt new file mode 100644 index 0000000..97c5c27 --- /dev/null +++ b/storm-auth/src/main/resources/banner.txt @@ -0,0 +1,10 @@ +Spring Boot Version: ${spring-boot.version} +Spring Application Name: ${spring.application.name} + _ _ _ + (_) | | | | + _ __ _ _ ___ _ _ _ ______ __ _ _ _ | |_ | |__ +| '__|| | | | / _ \ | | | || ||______| / _` || | | || __|| '_ \ +| | | |_| || (_) || |_| || | | (_| || |_| || |_ | | | | +|_| \__,_| \___/ \__, ||_| \__,_| \__,_| \__||_| |_| + __/ | + |___/ \ No newline at end of file diff --git a/storm-auth/src/main/resources/bootstrap.yml b/storm-auth/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..e84169a --- /dev/null +++ b/storm-auth/src/main/resources/bootstrap.yml @@ -0,0 +1,32 @@ +# Tomcat +server: + port: 9200 + +# Spring +spring: + application: + # 应用名称 + name: storm-auth + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 127.0.0.1:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: 127.0.0.1:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} diff --git a/storm-auth/src/main/resources/bootstrap.yml01 b/storm-auth/src/main/resources/bootstrap.yml01 new file mode 100644 index 0000000..6de3ae9 --- /dev/null +++ b/storm-auth/src/main/resources/bootstrap.yml01 @@ -0,0 +1,32 @@ +# Tomcat +server: + port: 9200 + +# Spring +spring: + application: + # 应用名称 + name: storm-auth + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} diff --git a/storm-auth/src/main/resources/logback.xml b/storm-auth/src/main/resources/logback.xml new file mode 100644 index 0000000..fd45851 --- /dev/null +++ b/storm-auth/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/storm-common/pom.xml b/storm-common/pom.xml new file mode 100644 index 0000000..2517ac2 --- /dev/null +++ b/storm-common/pom.xml @@ -0,0 +1,30 @@ + + + + com.storm + storm + 3.6.6 + + 4.0.0 + + + storm-common-log + storm-common-core + storm-common-redis + storm-common-swagger + storm-common-security + storm-common-sensitive + storm-common-datascope + storm-common-datasource + storm-common-oss + + + storm-common + pom + + + storm-common通用模块 + + + diff --git a/storm-common/storm-common-core/pom.xml b/storm-common/storm-common-core/pom.xml new file mode 100644 index 0000000..a155594 --- /dev/null +++ b/storm-common/storm-common-core/pom.xml @@ -0,0 +1,133 @@ + + + + com.storm + storm-common + 3.6.6 + + 4.0.0 + + storm-common-core + + + storm-common-core核心模块 + + + + + + + com.baomidou + mybatis-plus-boot-starter + + + + com.baomidou + mybatis-plus-annotation + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + + org.springframework + spring-context-support + + + + + org.springframework + spring-web + + + + + com.alibaba + transmittable-thread-local + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.alibaba.fastjson2 + fastjson2 + + + + + io.jsonwebtoken + jjwt + + + + + javax.xml.bind + jaxb-api + + + + + org.apache.commons + commons-lang3 + + + + + commons-io + commons-io + + + + + org.apache.poi + poi-ooxml + + + + + javax.servlet + javax.servlet-api + + + + cn.hutool + hutool-all + + + + com.storm + storm-common-swagger + + + + + diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/annotation/Excel.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/annotation/Excel.java new file mode 100644 index 0000000..f2caf65 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/annotation/Excel.java @@ -0,0 +1,187 @@ +package com.storm.common.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigDecimal; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import com.storm.common.core.utils.poi.ExcelHandlerAdapter; + +/** + * 自定义导出Excel数据注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Excel +{ + /** + * 导出时在excel中排序 + */ + public int sort() default Integer.MAX_VALUE; + + /** + * 导出到Excel中的名字. + */ + public String name() default ""; + + /** + * 日期格式, 如: yyyy-MM-dd + */ + public String dateFormat() default ""; + + /** + * 读取内容转表达式 (如: 0=男,1=女,2=未知) + */ + public String readConverterExp() default ""; + + /** + * 分隔符,读取字符串组内容 + */ + public String separator() default ","; + + /** + * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化) + */ + public int scale() default -1; + + /** + * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN + */ + public int roundingMode() default BigDecimal.ROUND_HALF_EVEN; + + /** + * 导出时在excel中每个列的高度 + */ + public double height() default 14; + + /** + * 导出时在excel中每个列的宽度 + */ + public double width() default 16; + + /** + * 文字后缀,如% 90 变成90% + */ + public String suffix() default ""; + + /** + * 当值为空时,字段的默认值 + */ + public String defaultValue() default ""; + + /** + * 提示信息 + */ + public String prompt() default ""; + + /** + * 是否允许内容换行 + */ + public boolean wrapText() default false; + + /** + * 设置只能选择不能输入的列内容. + */ + public String[] combo() default {}; + + /** + * 是否需要纵向合并单元格,应对需求:含有list集合单元格) + */ + public boolean needMerge() default false; + + /** + * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写. + */ + public boolean isExport() default true; + + /** + * 另一个类中的属性名称,支持多级获取,以小数点隔开 + */ + public String targetAttr() default ""; + + /** + * 是否自动统计数据,在最后追加一行统计数据总和 + */ + public boolean isStatistics() default false; + + /** + * 导出类型(0数字 1字符串) + */ + public ColumnType cellType() default ColumnType.STRING; + + /** + * 导出列头背景颜色 + */ + public IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT; + + /** + * 导出列头字体颜色 + */ + public IndexedColors headerColor() default IndexedColors.WHITE; + + /** + * 导出单元格背景颜色 + */ + public IndexedColors backgroundColor() default IndexedColors.WHITE; + + /** + * 导出单元格字体颜色 + */ + public IndexedColors color() default IndexedColors.BLACK; + + /** + * 导出字段对齐方式 + */ + public HorizontalAlignment align() default HorizontalAlignment.CENTER; + + /** + * 自定义数据处理器 + */ + public Class handler() default ExcelHandlerAdapter.class; + + /** + * 自定义数据处理器参数 + */ + public String[] args() default {}; + + /** + * 字段类型(0:导出导入;1:仅导出;2:仅导入) + */ + Type type() default Type.ALL; + + public enum Type + { + ALL(0), EXPORT(1), IMPORT(2); + private final int value; + + Type(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } + + public enum ColumnType + { + NUMERIC(0), STRING(1), IMAGE(2), TEXT(3); + private final int value; + + ColumnType(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/annotation/Excels.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/annotation/Excels.java new file mode 100644 index 0000000..3f0c01b --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/annotation/Excels.java @@ -0,0 +1,18 @@ +package com.storm.common.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Excel注解集 + * + * @author ruoyi + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Excels +{ + Excel[] value(); +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/CacheConstants.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/CacheConstants.java new file mode 100644 index 0000000..f7f610f --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/CacheConstants.java @@ -0,0 +1,66 @@ +package com.storm.common.core.constant; + +/** + * 缓存常量信息 + * + * @author ruoyi + */ +public class CacheConstants +{ + /** + * 缓存有效期,默认720(分钟) + */ + public final static long EXPIRATION = 10080; + + /** + * 缓存刷新时间,默认120(分钟) + */ + public final static long REFRESH_TIME = 120; + + /** + * 密码最大错误次数 + */ + public final static int PASSWORD_MAX_RETRY_COUNT = 5; + + /** + * 密码锁定时间,默认10(分钟) + */ + public final static long PASSWORD_LOCK_TIME = 10; + + /** + * 权限缓存前缀 + */ + public final static String LOGIN_TOKEN_KEY = "login_tokens:"; + + /** + * 验证码 redis key + */ + public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** + * 参数管理 cache key + */ + public static final String SYS_CONFIG_KEY = "sys_config:"; + + /** + * 字典管理 cache key + */ + public static final String SYS_DICT_KEY = "sys_dict:"; + + /** + * 登录账户密码错误次数 redis key + */ + public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:"; + + /** + * 登录IP黑名单 cache key + */ + public static final String SYS_LOGIN_BLACKIPLIST = SYS_CONFIG_KEY + "sys.login.blackIPList"; + + public static final String IOT_ALL_PRODUCT_LIST = "iot:all_product"; + public static final String IOT_DEVICE_LAST_DATA = "iot:device_last_data"; + public static final String IOT_ALERT_SILENT_PREFIX = "iot:alert_silent:"; + public static final String IOT_ALERT_TRIGGER_COUNT_PREFIX = "iot:alert_trigger_count:"; + public static final String IOT_DEVICE_ONLINE_TIME = "iot:device_online_time"; + public static final String IOT_DEVICE_MASSAGE_TIME = "iot:device_massage_time" ; +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/Constant.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/Constant.java new file mode 100644 index 0000000..601ec71 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/Constant.java @@ -0,0 +1,41 @@ +package com.storm.common.core.constant; + +public interface Constant { + String REQUEST_ID_HEADER = "requestId"; + String REQUEST_FROM_HEADER = "x-request-from"; + + String GATEWAY_ORIGIN_NAME = "gateway"; + String FEIGN_ORIGIN_NAME = "feign"; + + // 数据字段 - id + String DATA_FIELD_NAME_ID = "id"; + + // 数据字段 - create_time + String DATA_FIELD_NAME_CREATE_TIME = "create_time"; + String DATA_FIELD_NAME_CREATE_TIME_CAMEL = "createTime"; + + // 数据字段 - update_time + String DATA_FIELD_NAME_UPDATE_TIME = "update_time"; + String DATA_FIELD_NAME_UPDATE_TIME_CAMEL = "updateTime"; + + // 数据字段 - liked_times + String DATA_FIELD_NAME_LIKED_TIME = "liked_times"; + String DATA_FIELD_NAME_LIKED_TIME_CAMEL = "likedTimes"; + + // 数据字段 - creater + String DATA_FIELD_NAME_CREATER = "creater"; + + // 数据字段 - updater + String DATA_FIELD_NAME_UPDATER = "updater"; + + // 数据已经删除标识值 + boolean DATA_DELETE = true; + // 数据未删除标识值 + boolean DATA_NOT_DELETE = false; + // 响应结果是否被R标记过 + String BODY_PROCESSED_MARK_HEADER = "IS_BODY_PROCESSED"; + + String SPRING_AI_ATTR = "SpringAI"; + String SPRING_AI_FLAG = "yes"; + +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/Constants.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/Constants.java new file mode 100644 index 0000000..74346fc --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/Constants.java @@ -0,0 +1,135 @@ +package com.storm.common.core.constant; + +/** + * 通用常量信息 + * + * @author ruoyi + */ +public class Constants +{ + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * www主域 + */ + public static final String WWW = "www."; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 成功标记 + */ + public static final Integer SUCCESS = 200; + + /** + * 失败标记 + */ + public static final Integer FAIL = 500; + + /** + * 登录成功状态 + */ + public static final String LOGIN_SUCCESS_STATUS = "0"; + + /** + * 登录失败状态 + */ + public static final String LOGIN_FAIL_STATUS = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 当前记录起始索引 + */ + public static final String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + public static final String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + public static final String IS_ASC = "isAsc"; + + /** + * 验证码有效期(分钟) + */ + public static final long CAPTCHA_EXPIRATION = 2; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全) + */ + public static final String[] JSON_WHITELIST_STR = { "com.storm" }; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = { "com.storm.job.task" }; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "com.storm.common.core.utils.file" }; +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/GenConstants.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/GenConstants.java new file mode 100644 index 0000000..5ecc900 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/GenConstants.java @@ -0,0 +1,126 @@ +package com.storm.common.core.constant; + +/** + * 代码生成通用常量 + * + * @author ruoyi + */ +public class GenConstants +{ + /** 单表(增删改查) */ + public static final String TPL_CRUD = "crud"; + + /** 树表(增删改查) */ + public static final String TPL_TREE = "tree"; + + /** 主子表(增删改查) */ + public static final String TPL_SUB = "sub"; + + /** 树编码字段 */ + public static final String TREE_CODE = "treeCode"; + + /** 树父编码字段 */ + public static final String TREE_PARENT_CODE = "treeParentCode"; + + /** 树名称字段 */ + public static final String TREE_NAME = "treeName"; + + /** 上级菜单ID字段 */ + public static final String PARENT_MENU_ID = "parentMenuId"; + + /** 上级菜单名称字段 */ + public static final String PARENT_MENU_NAME = "parentMenuName"; + + /** 数据库字符串类型 */ + public static final String[] COLUMNTYPE_STR = { "char", "varchar", "nvarchar", "varchar2" }; + + /** 数据库文本类型 */ + public static final String[] COLUMNTYPE_TEXT = { "tinytext", "text", "mediumtext", "longtext" }; + + /** 数据库时间类型 */ + public static final String[] COLUMNTYPE_TIME = { "datetime", "time", "date", "timestamp" }; + + /** 数据库数字类型 */ + public static final String[] COLUMNTYPE_NUMBER = { "tinyint", "smallint", "mediumint", "int", "number", "integer", + "bit", "bigint", "float", "double", "decimal" }; + + /** 页面不需要编辑字段 */ + public static final String[] COLUMNNAME_NOT_EDIT = { "id", "create_by", "create_time", "del_flag" }; + + /** 页面不需要显示的列表字段 */ + public static final String[] COLUMNNAME_NOT_LIST = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time" }; + + /** 页面不需要查询字段 */ + public static final String[] COLUMNNAME_NOT_QUERY = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time", "remark" }; + + /** Entity基类字段 */ + public static final String[] BASE_ENTITY = { "createBy", "createTime", "updateBy", "updateTime", "remark" }; + + /** Tree基类字段 */ + public static final String[] TREE_ENTITY = { "parentName", "parentId", "orderNum", "ancestors" }; + + /** 文本框 */ + public static final String HTML_INPUT = "input"; + + /** 文本域 */ + public static final String HTML_TEXTAREA = "textarea"; + + /** 下拉框 */ + public static final String HTML_SELECT = "select"; + + /** 单选框 */ + public static final String HTML_RADIO = "radio"; + + /** 复选框 */ + public static final String HTML_CHECKBOX = "checkbox"; + + /** 日期控件 */ + public static final String HTML_DATETIME = "datetime"; + + /** JDK8时间类型 */ + public static final String TYPE_LOCAL_DATE_TIME = "LocalDateTime"; + + /** 图片上传控件 */ + public static final String HTML_IMAGE_UPLOAD = "imageUpload"; + + /** 文件上传控件 */ + public static final String HTML_FILE_UPLOAD = "fileUpload"; + + /** 富文本控件 */ + public static final String HTML_EDITOR = "editor"; + + /** 字符串类型 */ + public static final String TYPE_STRING = "String"; + + /** 整型 */ + public static final String TYPE_INTEGER = "Integer"; + + /** 长整型 */ + public static final String TYPE_LONG = "Long"; + + /** 浮点型 */ + public static final String TYPE_DOUBLE = "Double"; + + /** 高精度计算类型 */ + public static final String TYPE_BIGDECIMAL = "BigDecimal"; + + /** 时间类型 */ + public static final String TYPE_DATE = "Date"; + + /** 模糊查询 */ + public static final String QUERY_LIKE = "LIKE"; + + /** 相等查询 */ + public static final String QUERY_EQ = "EQ"; + + /** 需要 */ + public static final String REQUIRE = "1"; + + /** MySql tinyint 类型 */ + public static final String MYSQL_TINYINT = "tinyint"; + + /** MySql int 类型 */ + public static final String MYSQL_INT = "int"; +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/HttpStatus.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/HttpStatus.java new file mode 100644 index 0000000..6ea712a --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/HttpStatus.java @@ -0,0 +1,94 @@ +package com.storm.common.core.constant; + +/** + * 返回状态码 + * + * @author ruoyi + */ +public class HttpStatus +{ + /** + * 操作成功 + */ + public static final int SUCCESS = 200; + + /** + * 对象创建成功 + */ + public static final int CREATED = 201; + + /** + * 请求已经被接受 + */ + public static final int ACCEPTED = 202; + + /** + * 操作已经执行成功,但是没有返回数据 + */ + public static final int NO_CONTENT = 204; + + /** + * 资源已被移除 + */ + public static final int MOVED_PERM = 301; + + /** + * 重定向 + */ + public static final int SEE_OTHER = 303; + + /** + * 资源没有被修改 + */ + public static final int NOT_MODIFIED = 304; + + /** + * 参数列表错误(缺少,格式不匹配) + */ + public static final int BAD_REQUEST = 400; + + /** + * 未授权 + */ + public static final int UNAUTHORIZED = 401; + + /** + * 访问受限,授权过期 + */ + public static final int FORBIDDEN = 403; + + /** + * 资源,服务未找到 + */ + public static final int NOT_FOUND = 404; + + /** + * 不允许的http方法 + */ + public static final int BAD_METHOD = 405; + + /** + * 资源冲突,或者资源被锁 + */ + public static final int CONFLICT = 409; + + /** + * 不支持的数据,媒体类型 + */ + public static final int UNSUPPORTED_TYPE = 415; + + /** + * 系统内部错误 + */ + public static final int ERROR = 500; + + /** + * 接口未实现 + */ + public static final int NOT_IMPLEMENTED = 501; + + /** + * 系统警告消息 + */ + public static final int WARN = 601; +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/ScheduleConstants.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/ScheduleConstants.java new file mode 100644 index 0000000..eaadff6 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/ScheduleConstants.java @@ -0,0 +1,50 @@ +package com.storm.common.core.constant; + +/** + * 任务调度通用常量 + * + * @author ruoyi + */ +public class ScheduleConstants +{ + public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME"; + + /** 执行目标key */ + public static final String TASK_PROPERTIES = "TASK_PROPERTIES"; + + /** 默认 */ + public static final String MISFIRE_DEFAULT = "0"; + + /** 立即触发执行 */ + public static final String MISFIRE_IGNORE_MISFIRES = "1"; + + /** 触发一次执行 */ + public static final String MISFIRE_FIRE_AND_PROCEED = "2"; + + /** 不触发立即执行 */ + public static final String MISFIRE_DO_NOTHING = "3"; + + public enum Status + { + /** + * 正常 + */ + NORMAL("0"), + /** + * 暂停 + */ + PAUSE("1"); + + private String value; + + private Status(String value) + { + this.value = value; + } + + public String getValue() + { + return value; + } + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/SecurityConstants.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/SecurityConstants.java new file mode 100644 index 0000000..a28a774 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/SecurityConstants.java @@ -0,0 +1,49 @@ +package com.storm.common.core.constant; + +/** + * 权限相关通用常量 + * + * @author ruoyi + */ +public class SecurityConstants +{ + /** + * 用户ID字段 + */ + public static final String DETAILS_USER_ID = "user_id"; + + /** + * 用户名字段 + */ + public static final String DETAILS_USERNAME = "username"; + + /** + * 授权信息字段 + */ + public static final String AUTHORIZATION_HEADER = "Authorization"; + + /** + * 请求来源 + */ + public static final String FROM_SOURCE = "from-source"; + + /** + * 内部请求 + */ + public static final String INNER = "inner"; + + /** + * 用户标识 + */ + public static final String USER_KEY = "user_key"; + + /** + * 登录用户 + */ + public static final String LOGIN_USER = "login_user"; + + /** + * 角色权限 + */ + public static final String ROLE_PERMISSION = "role_permission"; +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/ServiceNameConstants.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/ServiceNameConstants.java new file mode 100644 index 0000000..bae4f3b --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/ServiceNameConstants.java @@ -0,0 +1,24 @@ +package com.storm.common.core.constant; + +/** + * 服务名称 + * + * @author ruoyi + */ +public class ServiceNameConstants +{ + /** + * 认证服务的serviceid + */ + public static final String AUTH_SERVICE = "storm-auth"; + + /** + * 系统模块的serviceid + */ + public static final String SYSTEM_SERVICE = "storm-system"; + + /** + * 文件服务的serviceid + */ + public static final String FILE_SERVICE = "storm-file"; +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/TokenConstants.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/TokenConstants.java new file mode 100644 index 0000000..4385d2a --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/TokenConstants.java @@ -0,0 +1,20 @@ +package com.storm.common.core.constant; + +/** + * Token的Key常量 + * + * @author ruoyi + */ +public class TokenConstants +{ + /** + * 令牌前缀 + */ + public static final String PREFIX = "Bearer "; + + /** + * 令牌秘钥 + */ + public final static String SECRET = "abcdefghijklmnopqrstuvwxyz"; + +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/UserConstants.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/UserConstants.java new file mode 100644 index 0000000..87f27a5 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/constant/UserConstants.java @@ -0,0 +1,88 @@ +package com.storm.common.core.constant; + +/** + * 用户常量信息 + * + * @author ruoyi + */ +public class UserConstants +{ + /** + * 平台内系统用户的唯一标志 + */ + public static final String SYS_USER = "SYS_USER"; + + /** 正常状态 */ + public static final String NORMAL = "0"; + + /** 异常状态 */ + public static final String EXCEPTION = "1"; + + /** 用户封禁状态 */ + public static final String USER_DISABLE = "1"; + + /** 角色正常状态 */ + public static final String ROLE_NORMAL = "0"; + + /** 角色封禁状态 */ + public static final String ROLE_DISABLE = "1"; + + /** 部门正常状态 */ + public static final String DEPT_NORMAL = "0"; + + /** 部门停用状态 */ + public static final String DEPT_DISABLE = "1"; + + /** 字典正常状态 */ + public static final String DICT_NORMAL = "0"; + + /** 是否为系统默认(是) */ + public static final String YES = "Y"; + + /** 是否菜单外链(是) */ + public static final String YES_FRAME = "0"; + + /** 是否菜单外链(否) */ + public static final String NO_FRAME = "1"; + + /** 菜单类型(目录) */ + public static final String TYPE_DIR = "M"; + + /** 菜单类型(菜单) */ + public static final String TYPE_MENU = "C"; + + /** 菜单类型(按钮) */ + public static final String TYPE_BUTTON = "F"; + + /** Layout组件标识 */ + public final static String LAYOUT = "Layout"; + + /** ParentView组件标识 */ + public final static String PARENT_VIEW = "ParentView"; + + /** InnerLink组件标识 */ + public final static String INNER_LINK = "InnerLink"; + + /** 校验是否唯一的返回标识 */ + public final static boolean UNIQUE = true; + public final static boolean NOT_UNIQUE = false; + + /** + * 用户名长度限制 + */ + public static final int USERNAME_MIN_LENGTH = 2; + + public static final int USERNAME_MAX_LENGTH = 20; + + /** + * 密码长度限制 + */ + public static final int PASSWORD_MIN_LENGTH = 5; + + public static final int PASSWORD_MAX_LENGTH = 20; + + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/context/SecurityContextHolder.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/context/SecurityContextHolder.java new file mode 100644 index 0000000..1bd6c78 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/context/SecurityContextHolder.java @@ -0,0 +1,98 @@ +package com.storm.common.core.context; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import com.alibaba.ttl.TransmittableThreadLocal; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.StringUtils; + +/** + * 获取当前线程变量中的 用户id、用户名称、Token等信息 + * 注意: 必须在网关通过请求头的方法传入,同时在HeaderInterceptor拦截器设置值。 否则这里无法获取 + * + * @author ruoyi + */ +public class SecurityContextHolder +{ + private static final TransmittableThreadLocal> THREAD_LOCAL = new TransmittableThreadLocal<>(); + + public static void set(String key, Object value) + { + Map map = getLocalMap(); + map.put(key, value == null ? StringUtils.EMPTY : value); + } + + public static String get(String key) + { + Map map = getLocalMap(); + return Convert.toStr(map.getOrDefault(key, StringUtils.EMPTY)); + } + + public static T get(String key, Class clazz) + { + Map map = getLocalMap(); + return StringUtils.cast(map.getOrDefault(key, null)); + } + + public static Map getLocalMap() + { + Map map = THREAD_LOCAL.get(); + if (map == null) + { + map = new ConcurrentHashMap(); + THREAD_LOCAL.set(map); + } + return map; + } + + public static void setLocalMap(Map threadLocalMap) + { + THREAD_LOCAL.set(threadLocalMap); + } + + public static Long getUserId() + { + return Convert.toLong(get(SecurityConstants.DETAILS_USER_ID), 0L); + } + + public static void setUserId(String account) + { + set(SecurityConstants.DETAILS_USER_ID, account); + } + + public static String getUserName() + { + return get(SecurityConstants.DETAILS_USERNAME); + } + + public static void setUserName(String username) + { + set(SecurityConstants.DETAILS_USERNAME, username); + } + + public static String getUserKey() + { + return get(SecurityConstants.USER_KEY); + } + + public static void setUserKey(String userKey) + { + set(SecurityConstants.USER_KEY, userKey); + } + + public static String getPermission() + { + return get(SecurityConstants.ROLE_PERMISSION); + } + + public static void setPermission(String permissions) + { + set(SecurityConstants.ROLE_PERMISSION, permissions); + } + + public static void remove() + { + THREAD_LOCAL.remove(); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/domain/R.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/domain/R.java new file mode 100644 index 0000000..170812a --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/domain/R.java @@ -0,0 +1,115 @@ +package com.storm.common.core.domain; + +import java.io.Serializable; +import com.storm.common.core.constant.Constants; + +/** + * 响应信息主体 + * + * @author ruoyi + */ +public class R implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 成功 */ + public static final int SUCCESS = Constants.SUCCESS; + + /** 失败 */ + public static final int FAIL = Constants.FAIL; + + private int code; + + private String msg; + + private T data; + + public static R ok() + { + return restResult(null, SUCCESS, null); + } + + public static R ok(T data) + { + return restResult(data, SUCCESS, null); + } + + public static R ok(T data, String msg) + { + return restResult(data, SUCCESS, msg); + } + + public static R fail() + { + return restResult(null, FAIL, null); + } + + public static R fail(String msg) + { + return restResult(null, FAIL, msg); + } + + public static R fail(T data) + { + return restResult(data, FAIL, null); + } + + public static R fail(T data, String msg) + { + return restResult(data, FAIL, msg); + } + + public static R fail(int code, String msg) + { + return restResult(null, code, msg); + } + + private static R restResult(T data, int code, String msg) + { + R apiResult = new R<>(); + apiResult.setCode(code); + apiResult.setData(data); + apiResult.setMsg(msg); + return apiResult; + } + + public int getCode() + { + return code; + } + + public void setCode(int code) + { + this.code = code; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public T getData() + { + return data; + } + + public void setData(T data) + { + this.data = data; + } + + public static Boolean isError(R ret) + { + return !isSuccess(ret); + } + + public static Boolean isSuccess(R ret) + { + return R.SUCCESS == ret.getCode(); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/domain/dto/PageDTO.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/domain/dto/PageDTO.java new file mode 100644 index 0000000..2a5fbfe --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/domain/dto/PageDTO.java @@ -0,0 +1,74 @@ +package com.storm.common.core.domain.dto; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import com.storm.common.core.utils.BeanUtils; +import com.storm.common.core.utils.CollUtils; +import com.storm.common.core.utils.Convert; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "分页结果") +public class PageDTO { + @Schema(description = "总条数") + protected Long total; + @Schema(description = "总页码数") + protected Long pages; + @Schema(description = "当前页数据") + protected List list; + + public static PageDTO empty(Long total, Long pages) { + return new PageDTO<>(total, pages, CollUtils.emptyList()); + } + public static PageDTO empty(Page page) { + return new PageDTO<>(page.getTotal(), page.getPages(), CollUtils.emptyList()); + } + + public static PageDTO of(Page page) { + if(page == null){ + return new PageDTO<>(); + } + if (CollUtils.isEmpty(page.getRecords())) { + return empty(page); + } + return new PageDTO<>(page.getTotal(), page.getPages(), page.getRecords()); + } + public static PageDTO of(Page page, Function mapper) { + if(page == null){ + return new PageDTO<>(); + } + if (CollUtils.isEmpty(page.getRecords())) { + return empty(page); + } + return new PageDTO<>(page.getTotal(), page.getPages(), + page.getRecords().stream().map(mapper).collect(Collectors.toList())); + } + public static PageDTO of(Page page, List list) { + return new PageDTO<>(page.getTotal(), page.getPages(), list); + } + + public static PageDTO of(Page page, Class clazz) { + return new PageDTO<>(page.getTotal(), page.getPages(), BeanUtils.copyList(page.getRecords(), clazz)); + } + + public static PageDTO of(Page page, Class clazz, Convert convert) { + return new PageDTO<>(page.getTotal(), page.getPages(), BeanUtils.copyList(page.getRecords(), clazz, convert)); + } + + @Schema(hidden = true) + @JsonIgnore + public boolean isEmpty(){ + return list == null || list.size() == 0; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/domain/query/PageQuery.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/domain/query/PageQuery.java new file mode 100644 index 0000000..914be7b --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/domain/query/PageQuery.java @@ -0,0 +1,76 @@ +package com.storm.common.core.domain.query; + +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.storm.common.core.constant.Constant; +import com.storm.common.core.utils.StringUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.Min; + +@Data +@Schema(description = "分页请求参数") +@Accessors(chain = true) +public class PageQuery { + public static final Integer DEFAULT_PAGE_SIZE = 20; + public static final Integer DEFAULT_PAGE_NUM = 1; + + @Schema(description = "页码", example = "1") + @Min(value = 1, message = "页码不能小于1") + private Integer pageNo = DEFAULT_PAGE_NUM; + + @Schema(description = "每页大小", example = "5") + @Min(value = 1, message = "每页查询数量不能小于1") + private Integer pageSize = DEFAULT_PAGE_SIZE; + + @Schema(description = "是否升序", example = "true") + private Boolean isAsc = true; + + @Schema(description = "排序字段", example = "id") + private String sortBy; + + public int from(){ + return (pageNo - 1) * pageSize; + } + + public Page toMpPage(OrderItem ... orderItems) { + Page page = new Page<>(pageNo, pageSize); + // 是否手动指定排序方式 + if (orderItems != null && orderItems.length > 0) { + for (OrderItem orderItem : orderItems) { + page.addOrder(orderItem); + } + return page; + } + // 前端是否有排序字段 + if (StringUtils.isNotEmpty(sortBy)){ + OrderItem orderItem = new OrderItem(); + orderItem.setAsc(isAsc); + orderItem.setColumn(sortBy); + page.addOrder(orderItem); + } + return page; + } + + public Page toMpPage(String defaultSortBy, boolean isAsc) { + if (StringUtils.isBlank(sortBy)){ + sortBy = defaultSortBy; + this.isAsc = isAsc; + } + Page page = new Page<>(pageNo, pageSize); + OrderItem orderItem = new OrderItem(); + orderItem.setAsc(this.isAsc); + orderItem.setColumn(sortBy); + page.addOrder(orderItem); + return page; + } + public Page toMpPageDefaultSortByCreateTimeDesc() { + return toMpPage(Constant.DATA_FIELD_NAME_CREATE_TIME, false); + } + + public Page toMpPageDefaultSortByUpdateTimeDesc() { + return toMpPage(Constant.DATA_FIELD_NAME_UPDATE_TIME, false); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/enums/UserStatus.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/enums/UserStatus.java new file mode 100644 index 0000000..b62f4e7 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/enums/UserStatus.java @@ -0,0 +1,30 @@ +package com.storm.common.core.enums; + +/** + * 用户状态 + * + * @author ruoyi + */ +public enum UserStatus +{ + OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除"); + + private final String code; + private final String info; + + UserStatus(String code, String info) + { + this.code = code; + this.info = info; + } + + public String getCode() + { + return code; + } + + public String getInfo() + { + return info; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/CaptchaException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/CaptchaException.java new file mode 100644 index 0000000..4ac3138 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/CaptchaException.java @@ -0,0 +1,16 @@ +package com.storm.common.core.exception; + +/** + * 验证码错误异常类 + * + * @author ruoyi + */ +public class CaptchaException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public CaptchaException(String msg) + { + super(msg); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/CheckedException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/CheckedException.java new file mode 100644 index 0000000..14e65ac --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/CheckedException.java @@ -0,0 +1,31 @@ +package com.storm.common.core.exception; + +/** + * 检查异常 + * + * @author ruoyi + */ +public class CheckedException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public CheckedException(String message) + { + super(message); + } + + public CheckedException(Throwable cause) + { + super(cause); + } + + public CheckedException(String message, Throwable cause) + { + super(message, cause); + } + + public CheckedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) + { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/DemoModeException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/DemoModeException.java new file mode 100644 index 0000000..9c9fdd6 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/DemoModeException.java @@ -0,0 +1,15 @@ +package com.storm.common.core.exception; + +/** + * 演示模式异常 + * + * @author ruoyi + */ +public class DemoModeException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public DemoModeException() + { + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/GlobalException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/GlobalException.java new file mode 100644 index 0000000..1b958de --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/GlobalException.java @@ -0,0 +1,58 @@ +package com.storm.common.core.exception; + +/** + * 全局异常 + * + * @author ruoyi + */ +public class GlobalException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public GlobalException() + { + } + + public GlobalException(String message) + { + this.message = message; + } + + public String getDetailMessage() + { + return detailMessage; + } + + public GlobalException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } + + @Override + public String getMessage() + { + return message; + } + + public GlobalException setMessage(String message) + { + this.message = message; + return this; + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/InnerAuthException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/InnerAuthException.java new file mode 100644 index 0000000..653266e --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/InnerAuthException.java @@ -0,0 +1,16 @@ +package com.storm.common.core.exception; + +/** + * 内部认证异常 + * + * @author ruoyi + */ +public class InnerAuthException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public InnerAuthException(String message) + { + super(message); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/PreAuthorizeException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/PreAuthorizeException.java new file mode 100644 index 0000000..2cb9efe --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/PreAuthorizeException.java @@ -0,0 +1,15 @@ +package com.storm.common.core.exception; + +/** + * 权限异常 + * + * @author ruoyi + */ +public class PreAuthorizeException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public PreAuthorizeException() + { + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/ServiceException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/ServiceException.java new file mode 100644 index 0000000..f4993ef --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/ServiceException.java @@ -0,0 +1,74 @@ +package com.storm.common.core.exception; + +/** + * 业务异常 + * + * @author ruoyi + */ +public final class ServiceException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private Integer code; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() + { + } + + public ServiceException(String message) + { + this.message = message; + } + + public ServiceException(String message, Integer code) + { + this.message = message; + this.code = code; + } + + public String getDetailMessage() + { + return detailMessage; + } + + @Override + public String getMessage() + { + return message; + } + + public Integer getCode() + { + return code; + } + + public ServiceException setMessage(String message) + { + this.message = message; + return this; + } + + public ServiceException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/UtilException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/UtilException.java new file mode 100644 index 0000000..25e4e06 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/UtilException.java @@ -0,0 +1,26 @@ +package com.storm.common.core.exception; + +/** + * 工具类异常 + * + * @author ruoyi + */ +public class UtilException extends RuntimeException +{ + private static final long serialVersionUID = 8247610319171014183L; + + public UtilException(Throwable e) + { + super(e.getMessage(), e); + } + + public UtilException(String message) + { + super(message); + } + + public UtilException(String message, Throwable throwable) + { + super(message, throwable); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/auth/NotLoginException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/auth/NotLoginException.java new file mode 100644 index 0000000..2379099 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/auth/NotLoginException.java @@ -0,0 +1,16 @@ +package com.storm.common.core.exception.auth; + +/** + * 未能通过的登录认证异常 + * + * @author ruoyi + */ +public class NotLoginException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public NotLoginException(String message) + { + super(message); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/auth/NotPermissionException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/auth/NotPermissionException.java new file mode 100644 index 0000000..c7a4699 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/auth/NotPermissionException.java @@ -0,0 +1,23 @@ +package com.storm.common.core.exception.auth; + +import org.apache.commons.lang3.StringUtils; + +/** + * 未能通过的权限认证异常 + * + * @author ruoyi + */ +public class NotPermissionException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public NotPermissionException(String permission) + { + super(permission); + } + + public NotPermissionException(String[] permissions) + { + super(StringUtils.join(permissions, ",")); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/auth/NotRoleException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/auth/NotRoleException.java new file mode 100644 index 0000000..2fec413 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/auth/NotRoleException.java @@ -0,0 +1,23 @@ +package com.storm.common.core.exception.auth; + +import org.apache.commons.lang3.StringUtils; + +/** + * 未能通过的角色认证异常 + * + * @author ruoyi + */ +public class NotRoleException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public NotRoleException(String role) + { + super(role); + } + + public NotRoleException(String[] roles) + { + super(StringUtils.join(roles, ",")); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/base/BaseException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/base/BaseException.java new file mode 100644 index 0000000..b8fc1c4 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/base/BaseException.java @@ -0,0 +1,79 @@ +package com.storm.common.core.exception.base; + +/** + * 基础异常 + * + * @author ruoyi + */ +public class BaseException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 所属模块 + */ + private String module; + + /** + * 错误码 + */ + private String code; + + /** + * 错误码对应的参数 + */ + private Object[] args; + + /** + * 错误消息 + */ + private String defaultMessage; + + public BaseException(String module, String code, Object[] args, String defaultMessage) + { + this.module = module; + this.code = code; + this.args = args; + this.defaultMessage = defaultMessage; + } + + public BaseException(String module, String code, Object[] args) + { + this(module, code, args, null); + } + + public BaseException(String module, String defaultMessage) + { + this(module, null, null, defaultMessage); + } + + public BaseException(String code, Object[] args) + { + this(null, code, args, null); + } + + public BaseException(String defaultMessage) + { + this(null, null, null, defaultMessage); + } + + public String getModule() + { + return module; + } + + public String getCode() + { + return code; + } + + public Object[] getArgs() + { + return args; + } + + public String getDefaultMessage() + { + return defaultMessage; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileException.java new file mode 100644 index 0000000..8410d30 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileException.java @@ -0,0 +1,19 @@ +package com.storm.common.core.exception.file; + +import com.storm.common.core.exception.base.BaseException; + +/** + * 文件信息异常类 + * + * @author ruoyi + */ +public class FileException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public FileException(String code, Object[] args, String msg) + { + super("file", code, args, msg); + } + +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileNameLengthLimitExceededException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileNameLengthLimitExceededException.java new file mode 100644 index 0000000..0a0d8a8 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileNameLengthLimitExceededException.java @@ -0,0 +1,16 @@ +package com.storm.common.core.exception.file; + +/** + * 文件名称超长限制异常类 + * + * @author ruoyi + */ +public class FileNameLengthLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileNameLengthLimitExceededException(int defaultFileNameLength) + { + super("upload.filename.exceed.length", new Object[] { defaultFileNameLength }, "the filename is too long"); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileSizeLimitExceededException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileSizeLimitExceededException.java new file mode 100644 index 0000000..6e49b86 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileSizeLimitExceededException.java @@ -0,0 +1,16 @@ +package com.storm.common.core.exception.file; + +/** + * 文件名大小限制异常类 + * + * @author ruoyi + */ +public class FileSizeLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileSizeLimitExceededException(long defaultMaxSize) + { + super("upload.exceed.maxSize", new Object[] { defaultMaxSize }, "the filesize is too large"); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileUploadException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileUploadException.java new file mode 100644 index 0000000..28c0b44 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/FileUploadException.java @@ -0,0 +1,61 @@ +package com.storm.common.core.exception.file; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * 文件上传异常类 + * + * @author ruoyi + */ +public class FileUploadException extends Exception +{ + + private static final long serialVersionUID = 1L; + + private final Throwable cause; + + public FileUploadException() + { + this(null, null); + } + + public FileUploadException(final String msg) + { + this(msg, null); + } + + public FileUploadException(String msg, Throwable cause) + { + super(msg); + this.cause = cause; + } + + @Override + public void printStackTrace(PrintStream stream) + { + super.printStackTrace(stream); + if (cause != null) + { + stream.println("Caused by:"); + cause.printStackTrace(stream); + } + } + + @Override + public void printStackTrace(PrintWriter writer) + { + super.printStackTrace(writer); + if (cause != null) + { + writer.println("Caused by:"); + cause.printStackTrace(writer); + } + } + + @Override + public Throwable getCause() + { + return cause; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/InvalidExtensionException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/InvalidExtensionException.java new file mode 100644 index 0000000..ebe68ba --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/file/InvalidExtensionException.java @@ -0,0 +1,80 @@ +package com.storm.common.core.exception.file; + +import java.util.Arrays; + +/** + * 文件上传 误异常类 + * + * @author ruoyi + */ +public class InvalidExtensionException extends FileUploadException +{ + private static final long serialVersionUID = 1L; + + private String[] allowedExtension; + private String extension; + private String filename; + + public InvalidExtensionException(String[] allowedExtension, String extension, String filename) + { + super("filename : [" + filename + "], extension : [" + extension + "], allowed extension : [" + Arrays.toString(allowedExtension) + "]"); + this.allowedExtension = allowedExtension; + this.extension = extension; + this.filename = filename; + } + + public String[] getAllowedExtension() + { + return allowedExtension; + } + + public String getExtension() + { + return extension; + } + + public String getFilename() + { + return filename; + } + + public static class InvalidImageExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidImageExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidFlashExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidFlashExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidMediaExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidMediaExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidVideoExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidVideoExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/job/TaskException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/job/TaskException.java new file mode 100644 index 0000000..cdebb31 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/job/TaskException.java @@ -0,0 +1,34 @@ +package com.storm.common.core.exception.job; + +/** + * 计划策略异常 + * + * @author ruoyi + */ +public class TaskException extends Exception +{ + private static final long serialVersionUID = 1L; + + private Code code; + + public TaskException(String msg, Code code) + { + this(msg, code, null); + } + + public TaskException(String msg, Code code, Exception nestedEx) + { + super(msg, nestedEx); + this.code = code; + } + + public Code getCode() + { + return code; + } + + public enum Code + { + TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/user/CaptchaExpireException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/user/CaptchaExpireException.java new file mode 100644 index 0000000..fa73550 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/user/CaptchaExpireException.java @@ -0,0 +1,16 @@ +package com.storm.common.core.exception.user; + +/** + * 验证码失效异常类 + * + * @author ruoyi + */ +public class CaptchaExpireException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaExpireException() + { + super("user.jcaptcha.expire", null); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/user/UserException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/user/UserException.java new file mode 100644 index 0000000..9a2bd91 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/user/UserException.java @@ -0,0 +1,18 @@ +package com.storm.common.core.exception.user; + +import com.storm.common.core.exception.base.BaseException; + +/** + * 用户信息异常类 + * + * @author ruoyi + */ +public class UserException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public UserException(String code, Object[] args) + { + super("user", code, args, null); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/user/UserPasswordNotMatchException.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/user/UserPasswordNotMatchException.java new file mode 100644 index 0000000..1850cc2 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/exception/user/UserPasswordNotMatchException.java @@ -0,0 +1,16 @@ +package com.storm.common.core.exception.user; + +/** + * 用户密码不正确或不符合规范异常类 + * + * @author ruoyi + */ +public class UserPasswordNotMatchException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordNotMatchException() + { + super("user.password.not.match", null); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/text/CharsetKit.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/text/CharsetKit.java new file mode 100644 index 0000000..62873cd --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/text/CharsetKit.java @@ -0,0 +1,86 @@ +package com.storm.common.core.text; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import com.storm.common.core.utils.StringUtils; + +/** + * 字符集工具类 + * + * @author ruoyi + */ +public class CharsetKit +{ + /** ISO-8859-1 */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** UTF-8 */ + public static final String UTF_8 = "UTF-8"; + /** GBK */ + public static final String GBK = "GBK"; + + /** ISO-8859-1 */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** UTF-8 */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** GBK */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) + { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) + { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) + { + if (null == srcCharset) + { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) + { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) + { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * @return 系统字符集编码 + */ + public static String systemCharset() + { + return Charset.defaultCharset().name(); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/text/Convert.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/text/Convert.java new file mode 100644 index 0000000..6558640 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/text/Convert.java @@ -0,0 +1,1018 @@ +package com.storm.common.core.text; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Set; +import com.storm.common.core.utils.StringUtils; + +/** + * 类型转换器 + * + * @author ruoyi + */ +public class Convert +{ + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof String) + { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) + { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof Character) + { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) + { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Byte) + { + return (Byte) value; + } + if (value instanceof Number) + { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Byte.parseByte(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) + { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Short) + { + return (Short) value; + } + if (value instanceof Number) + { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Short.parseShort(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) + { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Number) + { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return NumberFormat.getInstance().parse(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) + { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Integer) + { + return (Integer) value; + } + if (value instanceof Number) + { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Integer.parseInt(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) + { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) + { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) + { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Integer[] {}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Long[] {}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) + { + if (StringUtils.isEmpty(str)) + { + return new String[] {}; + } + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) + { + return str.split(split); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Long) + { + return (Long) value; + } + if (value instanceof Number) + { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) + { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Double) + { + return (Double) value; + } + if (value instanceof Number) + { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) + { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Float) + { + return (Float) value; + } + if (value instanceof Number) + { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Float.parseFloat(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) + { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no、1、0、是、否, 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Boolean) + { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) + { + case "true": + case "yes": + case "ok": + case "1": + case "是": + return true; + case "false": + case "no": + case "0": + case "否": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) + { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) + { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Enum.valueOf(clazz, valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) + { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigInteger) + { + return (BigInteger) value; + } + if (value instanceof Long) + { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigInteger(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) + { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigDecimal) + { + return (BigDecimal) value; + } + if (value instanceof Long) + { + return new BigDecimal((Long) value); + } + if (value instanceof Double) + { + return BigDecimal.valueOf((Double) value); + } + if (value instanceof Integer) + { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigDecimal(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) + { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) + { + return str(obj, CharsetKit.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) + { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) + { + if (null == obj) + { + return null; + } + + if (obj instanceof String) + { + return (String) obj; + } + else if (obj instanceof byte[] || obj instanceof Byte[]) + { + if (obj instanceof byte[]) + { + return str((byte[]) obj, charset); + } + else + { + Byte[] bytes = (Byte[]) obj; + int length = bytes.length; + byte[] dest = new byte[length]; + for (int i = 0; i < length; i++) + { + dest[i] = bytes[i]; + } + return str(dest, charset); + } + } + else if (obj instanceof ByteBuffer) + { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) + { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) + { + if (data == null) + { + return null; + } + + if (null == charset) + { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) + { + if (data == null) + { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) + { + if (null == charset) + { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) + { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) + { + char[] c = input.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') + { + c[i] = '\u3000'; + } + else if (c[i] < '\177') + { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) + { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) + { + char[] c = text.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') + { + c[i] = ' '; + } + else if (c[i] > '\uFF00' && c[i] < '\uFF5F') + { + c[i] = (char) (c[i] - 65248); + } + } + return new String(c); + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) + { + String[] fraction = { "角", "分" }; + String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; + String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } }; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) + { + // 优化double计算精度丢失问题 + BigDecimal nNum = new BigDecimal(n); + BigDecimal decimal = new BigDecimal(10); + BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN); + double d = scale.doubleValue(); + s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) + { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) + { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) + { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/text/StrFormatter.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/text/StrFormatter.java new file mode 100644 index 0000000..90eb12b --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/text/StrFormatter.java @@ -0,0 +1,92 @@ +package com.storm.common.core.text; + +import com.storm.common.core.utils.StringUtils; + +/** + * 字符串格式化 + * + * @author ruoyi + */ +public class StrFormatter +{ + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) + { + if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) + { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) + { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) + { + if (handledPosition == 0) + { + return strPattern; + } + else + { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } + else + { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) + { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) + { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + else + { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } + else + { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ArrayUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ArrayUtils.java new file mode 100644 index 0000000..1c994f9 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ArrayUtils.java @@ -0,0 +1,55 @@ +package com.storm.common.core.utils; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ArrayUtil; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 数组工具类 + * @ClassName ArrayUtils + * @author wusongsong + * @since 2022/7/10 12:02 + * @version 1.0.0 + **/ +public class ArrayUtils extends ArrayUtil { + + + + /** + * 将源数组转换成指定类型的列表 + * + * @param originList 原始列表 + * @param targetClazz 转换后列表元素的类型 + * @param 原始列表元素的类型 + * @param 目标列表元素的类型 + * @return 目标类型的集合 + */ + public static List convert(R[] originList, Class targetClazz) { + return convert(originList, targetClazz, null); + + } + + /** + * 将源数组转换成指定类型的列表 + * + * @param originList 原始列表 + * @param targetClazz 转换后列表元素的类型 + * @param convert 转换特殊字段接口 + * @param 原始列表元素的类型 + * @param 目标列表元素的类型 + * @return 目标类型的集合 + */ + public static List convert(R[] originList, Class targetClazz, Convert convert) { + if (isEmpty(originList)) { + return null; + } + + return Arrays.stream(originList) + .map(origin -> BeanUtils.copyBean(origin, targetClazz, convert)) + .collect(Collectors.toList()); + + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/BeanUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/BeanUtils.java new file mode 100644 index 0000000..63ed41e --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/BeanUtils.java @@ -0,0 +1,59 @@ +package com.storm.common.core.utils; + +import cn.hutool.core.bean.BeanUtil; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 继承自 hutool 的BeanUtil,增加了bean转换时自定义转换器的功能 + */ +public class BeanUtils extends BeanUtil { + + /** + * 将原对象转换成目标对象,对于字段不匹配的字段可以使用转换器处理 + * + * @param source 原对象 + * @param clazz 目标对象的class + * @param convert 转换器 + * @param 原对象类型 + * @param 目标对象类型 + * @return 目标对象 + */ + public static T copyBean(R source, Class clazz, Convert convert) { + T target = copyBean(source, clazz); + if (convert != null) { + convert.convert(source, target); + } + return target; + } + /** + * 将原对象转换成目标对象,对于字段不匹配的字段可以使用转换器处理 + * + * @param source 原对象 + * @param clazz 目标对象的class + * @param 原对象类型 + * @param 目标对象类型 + * @return 目标对象 + */ + public static T copyBean(R source, Class clazz){ + if (source == null) { + return null; + } + return toBean(source, clazz); + } + + public static List copyList(List list, Class clazz) { + if (list == null || list.size() == 0) { + return CollUtils.emptyList(); + } + return copyToList(list, clazz); + } + + public static List copyList(List list, Class clazz, Convert convert) { + if (list == null || list.size() == 0) { + return CollUtils.emptyList(); + } + return list.stream().map(r -> copyBean(r, clazz, convert)).collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/CollUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/CollUtils.java new file mode 100644 index 0000000..38a5d0d --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/CollUtils.java @@ -0,0 +1,142 @@ +package com.storm.common.core.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.IterUtil; +import com.storm.common.core.validate.Checker; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 继承自 hutool 的集合工具类 + */ +public class CollUtils extends CollectionUtil { + + public static List emptyList() { + return Collections.emptyList(); + } + + public static Set emptySet() { + return Collections.emptySet(); + } + + public static Map emptyMap() { + return Collections.emptyMap(); + } + + public static Set singletonSet(T t) { + return Collections.singleton(t); + } + + public static List singletonList(T t) { + return Collections.singletonList(t); + } + + public static List convertToInteger(List originList){ + return CollUtils.isNotEmpty(originList) ? originList.stream().map(NumberUtils::parseInt).collect(Collectors.toList()) : null; + } + + public static List convertToLong(List originLIst){ + return CollUtils.isNotEmpty(originLIst) ? originLIst.stream().map(NumberUtils::parseLong).collect(Collectors.toList()) : null; + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串 如果集合元素为数组、Iterable或Iterator,则递归组合其为字符串 + * @param collection 集合 + * @param conjunction 分隔符 + * @param 集合元素类型 + * @return 连接后的字符串 + * See Also: IterUtil.join(Iterator, CharSequence) + */ + public static String join(Collection collection, CharSequence conjunction) { + if (null == collection || collection.isEmpty()) { + return null; + } + return IterUtil.join(collection.iterator(), conjunction); + } + + public static String joinIgnoreNull(Collection collection, CharSequence conjunction) { + if (null == collection || collection.isEmpty()) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (T t : collection) { + if(t == null) continue; + sb.append(t).append(","); + } + if(sb.length() <= 0){ + return null; + } + return sb.deleteCharAt(sb.length() - 1).toString(); + } + + /** + * 集合校验逻辑 + * + * @param data 要校验的集合 + * @param checker 校验器 + * @param 集合元素类型 + */ + public static void check(List data, Checker checker){ + if(data == null){ + return; + } + for (T t : data){ + checker.check(t); + } + } + + /** + * 集合校验逻辑 + * + * @param data 要校验的集合 + * @param 集合元素类型 + */ + public static > void check(List data){ + if(data == null){ + return; + } + for (T t : data){ + t.check(); + } + } + + /** + * 将元素加入到集合中,为null的过滤掉 + * + * @param list 集合 + * @param data 要添加的数据 + * @param 元素类型 + */ + public static void add(Collection list, T... data) { + if (list == null || ArrayUtils.isEmpty(data)) { + return; + } + for (T t : data) { + if (ObjectUtils.isNotEmpty(t)) { + list.add(t); + } + } + } + //将两个集合出现次数相加 + public static Map union(Map map1, Map map2) { + if (CollUtils.isEmpty(map1)) { + return map2; + } else if (CollUtils.isEmpty(map2)) { + return map1; + } + for (Map.Entry entry : map1.entrySet()) { + Integer num = map2.get(entry.getKey()); + map2.put(entry.getKey(), NumberUtils.null2Zero(num) + entry.getValue()); + } + return map2; + } + + public static R getFiledOfFirst(List list, Function function) { + if (CollUtils.isEmpty(list)) { + return null; + } + return function.apply(list.get(0)); + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/Convert.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/Convert.java new file mode 100644 index 0000000..17cd291 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/Convert.java @@ -0,0 +1,8 @@ +package com.storm.common.core.utils; + +/** + * 对原对象进行计算,设置到目标对象中 + **/ +public interface Convert{ + void convert(R origin, T target); +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/DateUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/DateUtils.java new file mode 100644 index 0000000..5190f4b --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/DateUtils.java @@ -0,0 +1,276 @@ +package com.storm.common.core.utils; + +import java.lang.management.ManagementFactory; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang3.time.DateFormatUtils; + +/** + * 时间工具类 + * + * @author ruoyi + */ +public class DateUtils extends org.apache.commons.lang3.time.DateUtils +{ + public static String YYYY = "yyyy"; + + public static String YYYY_MM = "yyyy-MM"; + + public static String YYYY_MM_DD = "yyyy-MM-dd"; + + public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; + + public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + + private static String[] parsePatterns = { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", + "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", + "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"}; + + /** + * 获取当前Date型日期 + * + * @return Date() 当前日期 + */ + public static Date getNowDate() + { + return new Date(); + } + + /** + * 获取当前日期, 默认格式为yyyy-MM-dd + * + * @return String + */ + public static String getDate() + { + return dateTimeNow(YYYY_MM_DD); + } + + public static final String getTime() + { + return dateTimeNow(YYYY_MM_DD_HH_MM_SS); + } + + public static final String dateTimeNow() + { + return dateTimeNow(YYYYMMDDHHMMSS); + } + + public static final String dateTimeNow(final String format) + { + return parseDateToStr(format, new Date()); + } + + public static final String dateTime(final Date date) + { + return parseDateToStr(YYYY_MM_DD, date); + } + + public static final String parseDateToStr(final String format, final Date date) + { + return new SimpleDateFormat(format).format(date); + } + + public static final Date dateTime(final String format, final String ts) + { + try + { + return new SimpleDateFormat(format).parse(ts); + } + catch (ParseException e) + { + throw new RuntimeException(e); + } + } + + /** + * 日期路径 即年/月/日 如2018/08/08 + */ + public static final String datePath() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyy/MM/dd"); + } + + /** + * 日期路径 即年/月/日 如20180808 + */ + public static final String dateTime() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyyMMdd"); + } + + /** + * 日期型字符串转化为日期 格式 + */ + public static Date parseDate(Object str) + { + if (str == null) + { + return null; + } + try + { + return parseDate(str.toString(), parsePatterns); + } + catch (ParseException e) + { + return null; + } + } + + /** + * 获取服务器启动时间 + */ + public static Date getServerStartDate() + { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + /** + * 计算相差天数 + */ + public static int differentDaysByMillisecond(Date date1, Date date2) + { + return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24))); + } + + /** + * 计算时间差 + * + * @param endDate 最后时间 + * @param startTime 开始时间 + * @return 时间差(天/小时/分钟) + */ + public static String timeDistance(Date endDate, Date startTime) + { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + // long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - startTime.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + // long sec = diff % nd % nh % nm / ns; + return day + "天" + hour + "小时" + min + "分钟"; + } + + /** + * 增加 LocalDateTime ==> Date + */ + public static Date toDate(LocalDateTime temporalAccessor) + { + ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + /** + * 增加 LocalDate ==> Date + */ + public static Date toDate(LocalDate temporalAccessor) + { + LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0)); + ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + // 获取连续日期列表 + public static List getDateRange(String startDate, String endDate) { + List dates = new ArrayList<>(); + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + while (!start.isAfter(end)) { + dates.add(start.toString()); + start = start.plusDays(1); + } + return dates; + } + + /** + * 获取当天的开始时间 + */ + public static Date getTodayStart() { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + /** + * 获取本周的开始时间 + */ + public static Date getWeekStart() { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + /** + * 获取本月的开始时间 + */ + public static Date getMonthStart() { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.DAY_OF_MONTH, 1); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + /** + * 日期格式化 + */ + public static String format(Date date, String pattern) { + if (date == null) return null; + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + return sdf.format(date); + } + + /** + * 解析日期字符串 + */ + public static Date parse(String dateStr, String pattern) { + try { + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + return sdf.parse(dateStr); + } catch (Exception e) { + return null; + } + } + + /** + * 计算两个日期相差的天数 + */ + public static long daysBetween(Date start, Date end) { + long diff = end.getTime() - start.getTime(); + return diff / (1000 * 60 * 60 * 24); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ExceptionUtil.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ExceptionUtil.java new file mode 100644 index 0000000..9d42950 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ExceptionUtil.java @@ -0,0 +1,39 @@ +package com.storm.common.core.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * 错误信息处理类。 + * + * @author ruoyi + */ +public class ExceptionUtil +{ + /** + * 获取exception的详细错误信息。 + */ + public static String getExceptionMessage(Throwable e) + { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw, true)); + return sw.toString(); + } + + public static String getRootErrorMessage(Exception e) + { + Throwable root = ExceptionUtils.getRootCause(e); + root = (root == null ? e : root); + if (root == null) + { + return ""; + } + String msg = root.getMessage(); + if (msg == null) + { + return "null"; + } + return StringUtils.defaultString(msg); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/JwtUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/JwtUtils.java new file mode 100644 index 0000000..7d625fd --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/JwtUtils.java @@ -0,0 +1,123 @@ +package com.storm.common.core.utils; + +import java.util.Map; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.constant.TokenConstants; +import com.storm.common.core.text.Convert; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +/** + * Jwt工具类 + * + * @author ruoyi + */ +public class JwtUtils +{ + public static String secret = TokenConstants.SECRET; + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + public static String createToken(Map claims) + { + String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact(); + return token; + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + public static Claims parseToken(String token) + { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); + } + + /** + * 根据令牌获取用户标识 + * + * @param token 令牌 + * @return 用户ID + */ + public static String getUserKey(String token) + { + Claims claims = parseToken(token); + return getValue(claims, SecurityConstants.USER_KEY); + } + + /** + * 根据令牌获取用户标识 + * + * @param claims 身份信息 + * @return 用户ID + */ + public static String getUserKey(Claims claims) + { + return getValue(claims, SecurityConstants.USER_KEY); + } + + /** + * 根据令牌获取用户ID + * + * @param token 令牌 + * @return 用户ID + */ + public static String getUserId(String token) + { + Claims claims = parseToken(token); + return getValue(claims, SecurityConstants.DETAILS_USER_ID); + } + + /** + * 根据身份信息获取用户ID + * + * @param claims 身份信息 + * @return 用户ID + */ + public static String getUserId(Claims claims) + { + return getValue(claims, SecurityConstants.DETAILS_USER_ID); + } + + /** + * 根据令牌获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public static String getUserName(String token) + { + Claims claims = parseToken(token); + return getValue(claims, SecurityConstants.DETAILS_USERNAME); + } + + /** + * 根据身份信息获取用户名 + * + * @param claims 身份信息 + * @return 用户名 + */ + public static String getUserName(Claims claims) + { + return getValue(claims, SecurityConstants.DETAILS_USERNAME); + } + + /** + * 根据身份信息获取键值 + * + * @param claims 身份信息 + * @param key 键 + * @return 值 + */ + public static String getValue(Claims claims, String key) + { + return Convert.toStr(claims.get(key), ""); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/NumberUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/NumberUtils.java new file mode 100644 index 0000000..5c04071 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/NumberUtils.java @@ -0,0 +1,148 @@ +package com.storm.common.core.utils; + +import cn.hutool.core.util.NumberUtil; + +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class NumberUtils extends NumberUtil { + + + /** + * 如果number为空,将number转换为0,否则原数字返回 + * + * @param number 原数值 + * @return 整型数字,0或原数字 + */ + public static Integer null2Zero(Integer number){ + return number == null ? 0 : number; + } + + /** + * 如果number为空,将number转换为0,否则原数字返回 + * + * @param number 原数值 + * @return 整型数字,0或原数字 + */ + public static Double null2Zero(Double number){ + return number == null ? 0 : number; + } + + /** + * 如果number为空,将number转换为0L,否则原数字返回 + * + * @param number 原数值 + * @return 长整型数字,0L或原数字 + */ + public static Long null2Zero(Long number){ + return number == null ? 0L : number; + } + + + public static Double setScale(Double number) { + return new BigDecimal(number) + .setScale(2, BigDecimal.ROUND_HALF_UP) + .doubleValue(); + } + /** + * 比较两个数字是否相同, + * @param number1 数值1 + * @param number2 数值2 + * @return 是否一致 + */ + public static boolean equals(Integer number1, Integer number2) { + if(number1 == null || number2 == null){ + return false; + } + return number1.equals(number2); + } + + /** + * 数字除法保留指定小数位 + * @param num1 被除数 + * @param num2 除数 + * @param scale 小数点位数 + * @return 结果 + */ + public static Double divToDouble(Integer num1, Integer num2, int scale){ + if(num2 == null || num2 ==0 || num1 == null || num1 == 0) { + return 0d; + } + return div(num1, num2, scale).doubleValue(); + } + + public static Double max(List data){ + if(CollUtils.isEmpty(data)){ + return null; + } + return data.stream() + .max(Comparator.comparingDouble(num -> num)) + .orElse(0d); + } + public static Double min(List data){ + if(CollUtils.isEmpty(data)){ + return null; + } + return data.stream() + .min(Comparator.comparingDouble(num -> num)) + .orElse(0d); + } + + public static Double average(List data){ + if(CollUtils.isEmpty(data)){ + return 0d; + } + return data.stream() + .collect(Collectors.averagingDouble(Double::doubleValue)); + + } + + public static Integer toInt(Object obj) { + return obj == null ? null + : obj instanceof Integer + ? (int) obj : null; + } + + /** + * 取绝对值,如果为null,返回0 + * @param number 数值 + * @return 绝对值 + */ + public static int abs(Integer number) { + return number == null + ? 0 + : Math.abs(number); + } + + /** + * 数字格式化字符串,不足位数补0 + * + * @param originNumber 原始数字 + * @param digit 数字位数 + * @return 字符串 + */ + public static String repair0(Integer originNumber, Integer digit){ + StringBuilder number = new StringBuilder(originNumber + ""); + while (number.length() < digit) { + number.insert(0, "0"); + } + return number.toString(); + } + + + public static String scaleToStr(Integer num, int offset) { + // 1.计算位数 + int m = (int) Math.pow(10, offset); + // 2.计算商 + int s = num / m; + // 3.计算余数 + int y = num % m; + if (y == 0) { + return Integer.toString(s); + } + // 2.计算余数 + return s + "." + y; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ObjectUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ObjectUtils.java new file mode 100644 index 0000000..990fa3c --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ObjectUtils.java @@ -0,0 +1,84 @@ +package com.storm.common.core.utils; + +import cn.hutool.core.util.ObjectUtil; + +import java.lang.reflect.Field; +import java.math.BigDecimal; + +/** + * Object操作工具 + **/ +public class ObjectUtils extends ObjectUtil { + + /** + * 为object设置默认值,对target中的基本类型进行默认值初始化, + * 为null的对象不操作 + * + * @param target 需要初始化的对象 + */ + public static void setDefault(Object target) { + if (target == null) { + return; + } + Class clazz = target.getClass(); + Field[] declaredFields = clazz.getDeclaredFields(); + for (Field field : declaredFields) { + setDefault(field, target); + } + + } + + /** + * 给某个字段设置为默认值 + * + * @param field + * @param target + */ + private static void setDefault(Field field, Object target) { + field.setAccessible(true); + try { + Object value = field.get(target); + if (value != null) { + return; + } + String type = field.getGenericType().toString(); + Object defaultValue; + switch (type) { + case "class java.lang.String": + case "class java.lang.Character": + defaultValue = ""; + break; + case "class java.lang.Double": + defaultValue = 0.0d; + break; + case "class java.lang.Long": + defaultValue = 0L; + break; + case "class java.lang.Short": + defaultValue = (short) 0; + break; + case "class java.lang.Integer": + defaultValue = 0; + break; + case "class java.lang.Float": + defaultValue = 0f; + break; + case "class java.lang.Byte": + defaultValue = (byte) 0; + break; + case "class java.math.BigDecimal": + defaultValue = BigDecimal.ZERO; + break; + case "class java.lang.Boolean": + defaultValue = Boolean.FALSE; + break; + default: + defaultValue = null; + + } + field.set(target, defaultValue); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/PageUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/PageUtils.java new file mode 100644 index 0000000..7369e8f --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/PageUtils.java @@ -0,0 +1,35 @@ +package com.storm.common.core.utils; + +import com.github.pagehelper.PageHelper; +import com.storm.common.core.utils.sql.SqlUtil; +import com.storm.common.core.web.page.PageDomain; +import com.storm.common.core.web.page.TableSupport; + +/** + * 分页工具类 + * + * @author ruoyi + */ +public class PageUtils extends PageHelper +{ + /** + * 设置请求分页数据 + */ + public static void startPage() + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + Integer pageNum = pageDomain.getPageNum(); + Integer pageSize = pageDomain.getPageSize(); + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + Boolean reasonable = pageDomain.getReasonable(); + PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable); + } + + /** + * 清理分页的线程变量 + */ + public static void clearPage() + { + PageHelper.clearPage(); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ServletUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ServletUtils.java new file mode 100644 index 0000000..0dc05db --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ServletUtils.java @@ -0,0 +1,333 @@ +package com.storm.common.core.utils; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import com.alibaba.fastjson2.JSON; +import com.storm.common.core.constant.Constants; +import com.storm.common.core.domain.R; +import com.storm.common.core.text.Convert; +import reactor.core.publisher.Mono; + +/** + * 客户端工具类 + * + * @author ruoyi + */ +public class ServletUtils +{ + /** + * 获取String参数 + */ + public static String getParameter(String name) + { + return getRequest().getParameter(name); + } + + /** + * 获取String参数 + */ + public static String getParameter(String name, String defaultValue) + { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name) + { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name, Integer defaultValue) + { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name) + { + return Convert.toBool(getRequest().getParameter(name)); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name, Boolean defaultValue) + { + return Convert.toBool(getRequest().getParameter(name), defaultValue); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParams(ServletRequest request) + { + final Map map = request.getParameterMap(); + return Collections.unmodifiableMap(map); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParamMap(ServletRequest request) + { + Map params = new HashMap<>(); + for (Map.Entry entry : getParams(request).entrySet()) + { + params.put(entry.getKey(), StringUtils.join(entry.getValue(), ",")); + } + return params; + } + + /** + * 获取request + */ + public static HttpServletRequest getRequest() + { + try + { + return getRequestAttributes().getRequest(); + } + catch (Exception e) + { + return null; + } + } + + /** + * 获取response + */ + public static HttpServletResponse getResponse() + { + try + { + return getRequestAttributes().getResponse(); + } + catch (Exception e) + { + return null; + } + } + + /** + * 获取session + */ + public static HttpSession getSession() + { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() + { + try + { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } + catch (Exception e) + { + return null; + } + } + + public static String getHeader(HttpServletRequest request, String name) + { + String value = request.getHeader(name); + if (StringUtils.isEmpty(value)) + { + return StringUtils.EMPTY; + } + return urlDecode(value); + } + + public static Map getHeaders(HttpServletRequest request) + { + Map map = new LinkedCaseInsensitiveMap<>(); + Enumeration enumeration = request.getHeaderNames(); + if (enumeration != null) + { + while (enumeration.hasMoreElements()) + { + String key = enumeration.nextElement(); + String value = request.getHeader(key); + map.put(key, value); + } + } + return map; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + */ + public static void renderString(HttpServletResponse response, String string) + { + try + { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + /** + * 是否是Ajax异步请求 + * + * @param request + */ + public static boolean isAjaxRequest(HttpServletRequest request) + { + String accept = request.getHeader("accept"); + if (accept != null && accept.contains("application/json")) + { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) + { + return true; + } + + String uri = request.getRequestURI(); + if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) + { + return true; + } + + String ajax = request.getParameter("__ajax"); + return StringUtils.inStringIgnoreCase(ajax, "json", "xml"); + } + + /** + * 内容编码 + * + * @param str 内容 + * @return 编码后的内容 + */ + public static String urlEncode(String str) + { + try + { + return URLEncoder.encode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } + + /** + * 内容解码 + * + * @param str 内容 + * @return 解码后的内容 + */ + public static String urlDecode(String str) + { + try + { + return URLDecoder.decode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, Object value) + { + return webFluxResponseWriter(response, HttpStatus.OK, value, R.FAIL); + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param code 响应状态码 + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, Object value, int code) + { + return webFluxResponseWriter(response, HttpStatus.OK, value, code); + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param status http状态码 + * @param code 响应状态码 + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, HttpStatus status, Object value, int code) + { + return webFluxResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, status, value, code); + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param contentType content-type + * @param status http状态码 + * @param code 响应状态码 + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, String contentType, HttpStatus status, Object value, int code) + { + response.setStatusCode(status); + response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType); + R result = R.fail(code, value.toString()); + DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes()); + return response.writeWith(Mono.just(dataBuffer)); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/SpringUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/SpringUtils.java new file mode 100644 index 0000000..b3904cb --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/SpringUtils.java @@ -0,0 +1,114 @@ +package com.storm.common.core.utils; + +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.stereotype.Component; + +/** + * spring工具类 方便在非spring管理环境中获取bean + * + * @author ruoyi + */ +@Component +public final class SpringUtils implements BeanFactoryPostProcessor +{ + /** Spring应用上下文环境 */ + private static ConfigurableListableBeanFactory beanFactory; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException + { + SpringUtils.beanFactory = beanFactory; + } + + /** + * 获取对象 + * + * @param name + * @return Object 一个以所给名字注册的bean的实例 + * @throws org.springframework.beans.BeansException + * + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) throws BeansException + { + return (T) beanFactory.getBean(name); + } + + /** + * 获取类型为requiredType的对象 + * + * @param clz + * @return + * @throws org.springframework.beans.BeansException + * + */ + public static T getBean(Class clz) throws BeansException + { + T result = (T) beanFactory.getBean(clz); + return result; + } + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + * + * @param name + * @return boolean + */ + public static boolean containsBean(String name) + { + return beanFactory.containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + * @param name + * @return boolean + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.isSingleton(name); + } + + /** + * @param name + * @return Class 注册对象的类型 + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + * + * @param name + * @return + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getAliases(name); + } + + /** + * 获取aop代理对象 + * + * @param invoker + * @return + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) + { + return (T) AopContext.currentProxy(); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/StringUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/StringUtils.java new file mode 100644 index 0000000..56a7068 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/StringUtils.java @@ -0,0 +1,633 @@ +package com.storm.common.core.utils; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.springframework.util.AntPathMatcher; +import com.storm.common.core.constant.Constants; +import com.storm.common.core.text.StrFormatter; + +/** + * 字符串工具类 + * + * @author ruoyi + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils +{ + /** 空字符串 */ + private static final String NULLSTR = ""; + + /** 下划线 */ + private static final char SEPARATOR = '_'; + + /** 星号 */ + private static final char ASTERISK = '*'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) + { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) + { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) + { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + ** @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) + { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) + { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) + { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) + { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) + { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) + { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) + { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) + { + return !isNull(object); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) + { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) + { + return (str == null ? "" : str.trim()); + } + + /** + * 替换指定字符串的指定区间内字符为"*" + * + * @param str 字符串 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @return 替换后的字符串 + */ + public static String hide(CharSequence str, int startInclude, int endExclude) + { + if (isEmpty(str)) + { + return NULLSTR; + } + final int strLength = str.length(); + if (startInclude > strLength) + { + return NULLSTR; + } + if (endExclude > strLength) + { + endExclude = strLength; + } + if (startInclude > endExclude) + { + // 如果起始位置大于结束位置,不替换 + return NULLSTR; + } + final char[] chars = new char[strLength]; + for (int i = 0; i < strLength; i++) + { + if (i >= startInclude && i < endExclude) + { + chars[i] = ASTERISK; + } + else + { + chars[i] = str.charAt(i); + } + } + return new String(chars); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) + { + if (str == null) + { + return NULLSTR; + } + + if (start < 0) + { + start = str.length() + start; + } + + if (start < 0) + { + start = 0; + } + if (start > str.length()) + { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) + { + if (str == null) + { + return NULLSTR; + } + + if (end < 0) + { + end = str.length() + end; + } + if (start < 0) + { + start = str.length() + start; + } + + if (end > str.length()) + { + end = str.length(); + } + + if (start > end) + { + return NULLSTR; + } + + if (start < 0) + { + start = 0; + } + if (end < 0) + { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 在字符串中查找第一个出现的 `open` 和最后一个出现的 `close` 之间的子字符串 + * + * @param str 要截取的字符串 + * @param open 起始字符串 + * @param close 结束字符串 + * @return 截取结果 + */ + public static String substringBetweenLast(final String str, final String open, final String close) + { + if (isEmpty(str) || isEmpty(open) || isEmpty(close)) + { + return NULLSTR; + } + final int start = str.indexOf(open); + if (start != INDEX_NOT_FOUND) + { + final int end = str.lastIndexOf(close); + if (end != INDEX_NOT_FOUND) + { + return str.substring(start + open.length(), end); + } + } + return NULLSTR; + } + + /** + * 判断是否为空,并且不是空白字符 + * + * @param str 要判断的value + * @return 结果 + */ + public static boolean hasText(String str) + { + return (str != null && !str.isEmpty() && containsText(str)); + } + + private static boolean containsText(CharSequence str) + { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) + { + if (!Character.isWhitespace(str.charAt(i))) + { + return true; + } + } + return false; + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) + { + if (isEmpty(params) || isEmpty(template)) + { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 是否为http(s)://开头 + * + * @param link 链接 + * @return 结果 + */ + public static boolean ishttp(String link) + { + return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); + } + + /** + * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value + * + * @param collection 给定的集合 + * @param array 给定的数组 + * @return boolean 结果 + */ + public static boolean containsAny(Collection collection, String... array) + { + if (isEmpty(collection) || isEmpty(array)) + { + return false; + } + else + { + for (String str : array) + { + if (collection.contains(str)) + { + return true; + } + } + return false; + } + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) + { + if (str == null) + { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) + { + char c = str.charAt(i); + if (i > 0) + { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } + else + { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) + { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) + { + sb.append(SEPARATOR); + } + else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) + { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) + { + if (str != null && strs != null) + { + for (String s : strs) + { + if (str.equalsIgnoreCase(trim(s))) + { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) + { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) + { + // 没必要转换 + return ""; + } + else if (!name.contains("_")) + { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) + { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) + { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 + * 例如:user_name->userName + */ + public static String toCamelCase(String s) + { + if (s == null) + { + return null; + } + if (s.indexOf(SEPARATOR) == -1) + { + return s; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + + if (c == SEPARATOR) + { + upperCase = true; + } + else if (upperCase) + { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } + else + { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) + { + if (isEmpty(str) || isEmpty(strs)) + { + return false; + } + for (String pattern : strs) + { + if (isMatch(pattern, str)) + { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return + */ + public static boolean isMatch(String pattern, String url) + { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) + { + return (T) obj; + } + + /** + * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static final String padl(final Number num, final int size) + { + return padl(num.toString(), size, '0'); + } + + /** + * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static final String padl(final String s, final int size, final char c) + { + final StringBuilder sb = new StringBuilder(size); + if (s != null) + { + final int len = s.length(); + if (s.length() <= size) + { + for (int i = size - len; i > 0; i--) + { + sb.append(c); + } + sb.append(s); + } + else + { + return s.substring(len - size, len); + } + } + else + { + for (int i = size; i > 0; i--) + { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/bean/BeanUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/bean/BeanUtils.java new file mode 100644 index 0000000..a617a53 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/bean/BeanUtils.java @@ -0,0 +1,110 @@ +package com.storm.common.core.utils.bean; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Bean 工具类 + * + * @author ruoyi + */ +public class BeanUtils extends org.springframework.beans.BeanUtils +{ + /** Bean方法名中属性名开始的下标 */ + private static final int BEAN_METHOD_PROP_INDEX = 3; + + /** * 匹配getter方法的正则表达式 */ + private static final Pattern GET_PATTERN = Pattern.compile("get(\\p{javaUpperCase}\\w*)"); + + /** * 匹配setter方法的正则表达式 */ + private static final Pattern SET_PATTERN = Pattern.compile("set(\\p{javaUpperCase}\\w*)"); + + /** + * Bean属性复制工具方法。 + * + * @param dest 目标对象 + * @param src 源对象 + */ + public static void copyBeanProp(Object dest, Object src) + { + try + { + copyProperties(src, dest); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + /** + * 获取对象的setter方法。 + * + * @param obj 对象 + * @return 对象的setter方法列表 + */ + public static List getSetterMethods(Object obj) + { + // setter方法列表 + List setterMethods = new ArrayList(); + + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + + // 查找setter方法 + + for (Method method : methods) + { + Matcher m = SET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 1)) + { + setterMethods.add(method); + } + } + // 返回setter方法列表 + return setterMethods; + } + + /** + * 获取对象的getter方法。 + * + * @param obj 对象 + * @return 对象的getter方法列表 + */ + + public static List getGetterMethods(Object obj) + { + // getter方法列表 + List getterMethods = new ArrayList(); + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + // 查找getter方法 + for (Method method : methods) + { + Matcher m = GET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 0)) + { + getterMethods.add(method); + } + } + // 返回getter方法列表 + return getterMethods; + } + + /** + * 检查Bean方法名中的属性名是否相等。
+ * 如getName()和setName()属性名一样,getName()和setAge()属性名不一样。 + * + * @param m1 方法名1 + * @param m2 方法名2 + * @return 属性名一样返回true,否则返回false + */ + + public static boolean isMethodPropEquals(String m1, String m2) + { + return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX)); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/bean/BeanValidators.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/bean/BeanValidators.java new file mode 100644 index 0000000..3560a11 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/bean/BeanValidators.java @@ -0,0 +1,24 @@ +package com.storm.common.core.utils.bean; + +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; + +/** + * bean对象属性验证 + * + * @author ruoyi + */ +public class BeanValidators +{ + public static void validateWithException(Validator validator, Object object, Class... groups) + throws ConstraintViolationException + { + Set> constraintViolations = validator.validate(object, groups); + if (!constraintViolations.isEmpty()) + { + throw new ConstraintViolationException(constraintViolations); + } + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/FileTypeUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/FileTypeUtils.java new file mode 100644 index 0000000..3eadc8b --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/FileTypeUtils.java @@ -0,0 +1,95 @@ +package com.storm.common.core.utils.file; + +import java.io.File; +import java.util.Objects; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +/** + * 文件类型工具类 + * + * @author ruoyi + */ +public class FileTypeUtils +{ + /** + * 获取文件类型 + *

+ * 例如: ruoyi.txt, 返回: txt + * + * @param file 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(File file) + { + if (null == file) + { + return StringUtils.EMPTY; + } + return getFileType(file.getName()); + } + + /** + * 获取文件类型 + *

+ * 例如: ruoyi.txt, 返回: txt + * + * @param fileName 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(String fileName) + { + int separatorIndex = fileName.lastIndexOf("."); + if (separatorIndex < 0) + { + return ""; + } + return fileName.substring(separatorIndex + 1).toLowerCase(); + } + + /** + * 获取文件名的后缀 + * + * @param file 表单文件 + * @return 后缀名 + */ + public static final String getExtension(MultipartFile file) + { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (StringUtils.isEmpty(extension)) + { + extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType())); + } + return extension; + } + + /** + * 获取文件类型 + * + * @param photoByte 文件字节码 + * @return 后缀(不含".") + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "JPG"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "GIF"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "JPG"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "BMP"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "PNG"; + } + return strFileExtendName; + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/FileUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/FileUtils.java new file mode 100644 index 0000000..22e0d7c --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/FileUtils.java @@ -0,0 +1,253 @@ +package com.storm.common.core.utils.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import com.storm.common.core.utils.StringUtils; + +/** + * 文件处理工具类 + * + * @author ruoyi + */ +public class FileUtils +{ + /** 字符常量:斜杠 {@code '/'} */ + public static final char SLASH = '/'; + + /** 字符常量:反斜杠 {@code '\\'} */ + public static final char BACKSLASH = '\\'; + + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 输出指定文件的byte数组 + * + * @param filePath 文件路径 + * @param os 输出流 + * @return + */ + public static void writeBytes(String filePath, OutputStream os) throws IOException + { + FileInputStream fis = null; + try + { + File file = new File(filePath); + if (!file.exists()) + { + throw new FileNotFoundException(filePath); + } + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + int length; + while ((length = fis.read(b)) > 0) + { + os.write(b, 0, length); + } + } + catch (IOException e) + { + throw e; + } + finally + { + if (os != null) + { + try + { + os.close(); + } + catch (IOException e1) + { + e1.printStackTrace(); + } + } + if (fis != null) + { + try + { + fis.close(); + } + catch (IOException e1) + { + e1.printStackTrace(); + } + } + } + } + + /** + * 删除文件 + * + * @param filePath 文件 + * @return + */ + public static boolean deleteFile(String filePath) + { + boolean flag = false; + File file = new File(filePath); + // 路径为文件且不为空则进行删除 + if (file.isFile() && file.exists()) + { + flag = file.delete(); + } + return flag; + } + + /** + * 文件名称验证 + * + * @param filename 文件名称 + * @return true 正常 false 非法 + */ + public static boolean isValidFilename(String filename) + { + return filename.matches(FILENAME_PATTERN); + } + + /** + * 校验文件路径合法性(安全性与扩展名) + * + * @param fileUrl 待校验的文件地址 + * @return true 正常 false 非法 + */ + public static boolean validateFilePath(String fileUrl) + { + // 禁止目录上跳级别 + if (StringUtils.contains(fileUrl, "..")) + { + return false; + } + // 判断是否在允许下载的文件规则内 + return ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(fileUrl)); + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException + { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) + { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } + else if (agent.contains("Firefox")) + { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } + else if (agent.contains("Chrome")) + { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + else + { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } + + /** + * 返回文件名 + * + * @param filePath 文件 + * @return 文件名 + */ + public static String getName(String filePath) + { + if (null == filePath) + { + return null; + } + int len = filePath.length(); + if (0 == len) + { + return filePath; + } + if (isFileSeparator(filePath.charAt(len - 1))) + { + // 以分隔符结尾的去掉结尾分隔符 + len--; + } + + int begin = 0; + char c; + for (int i = len - 1; i > -1; i--) + { + c = filePath.charAt(i); + if (isFileSeparator(c)) + { + // 查找最后一个路径分隔符(/或者\) + begin = i + 1; + break; + } + } + + return filePath.substring(begin, len); + } + + /** + * 是否为Windows或者Linux(Unix)文件分隔符
+ * Windows平台下分隔符为\,Linux(Unix)为/ + * + * @param c 字符 + * @return 是否为Windows或者Linux(Unix)文件分隔符 + */ + public static boolean isFileSeparator(char c) + { + return SLASH == c || BACKSLASH == c; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + * @return + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException + { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException + { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/ImageUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/ImageUtils.java new file mode 100644 index 0000000..0d7f96e --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/ImageUtils.java @@ -0,0 +1,84 @@ +package com.storm.common.core.utils.file; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import org.apache.poi.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 图片处理工具类 + * + * @author ruoyi + */ +public class ImageUtils +{ + private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); + + public static byte[] getImage(String imagePath) + { + InputStream is = getFile(imagePath); + try + { + return IOUtils.toByteArray(is); + } + catch (Exception e) + { + log.error("图片加载异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(is); + } + } + + public static InputStream getFile(String imagePath) + { + try + { + byte[] result = readFile(imagePath); + result = Arrays.copyOf(result, result.length); + return new ByteArrayInputStream(result); + } + catch (Exception e) + { + log.error("获取图片异常 {}", e); + } + return null; + } + + /** + * 读取文件为字节数据 + * + * @param url 地址 + * @return 字节数据 + */ + public static byte[] readFile(String url) + { + InputStream in = null; + try + { + // 网络地址 + URL urlObj = new URL(url); + URLConnection urlConnection = urlObj.openConnection(); + urlConnection.setConnectTimeout(30 * 1000); + urlConnection.setReadTimeout(60 * 1000); + urlConnection.setDoInput(true); + in = urlConnection.getInputStream(); + return IOUtils.toByteArray(in); + } + catch (Exception e) + { + log.error("访问文件异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(in); + } + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/MimeTypeUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/MimeTypeUtils.java new file mode 100644 index 0000000..a1ee3b4 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/file/MimeTypeUtils.java @@ -0,0 +1,59 @@ +package com.storm.common.core.utils.file; + +/** + * 媒体类型工具类 + * + * @author ruoyi + */ +public class MimeTypeUtils +{ + public static final String IMAGE_PNG = "image/png"; + + public static final String IMAGE_JPG = "image/jpg"; + + public static final String IMAGE_JPEG = "image/jpeg"; + + public static final String IMAGE_BMP = "image/bmp"; + + public static final String IMAGE_GIF = "image/gif"; + + public static final String[] IMAGE_EXTENSION = { "bmp", "gif", "jpg", "jpeg", "png" }; + + public static final String[] FLASH_EXTENSION = { "swf", "flv" }; + + public static final String[] MEDIA_EXTENSION = { "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", + "asf", "rm", "rmvb" }; + + public static final String[] VIDEO_EXTENSION = { "mp4", "avi", "rmvb" }; + + public static final String[] DEFAULT_ALLOWED_EXTENSION = { + // 图片 + "bmp", "gif", "jpg", "jpeg", "png", + // word excel powerpoint + "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", + // 压缩文件 + "rar", "zip", "gz", "bz2", + // 视频格式 + "mp4", "avi", "rmvb", + // pdf + "pdf" }; + + public static String getExtension(String prefix) + { + switch (prefix) + { + case IMAGE_PNG: + return "png"; + case IMAGE_JPG: + return "jpg"; + case IMAGE_JPEG: + return "jpeg"; + case IMAGE_BMP: + return "bmp"; + case IMAGE_GIF: + return "gif"; + default: + return ""; + } + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/html/EscapeUtil.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/html/EscapeUtil.java new file mode 100644 index 0000000..43388f2 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/html/EscapeUtil.java @@ -0,0 +1,167 @@ +package com.storm.common.core.utils.html; + +import com.storm.common.core.utils.StringUtils; + +/** + * 转义和反转义工具类 + * + * @author ruoyi + */ +public class EscapeUtil +{ + public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)"; + + private static final char[][] TEXT = new char[64][]; + + static + { + for (int i = 0; i < 64; i++) + { + TEXT[i] = new char[] { (char) i }; + } + + // special HTML characters + TEXT['\''] = "'".toCharArray(); // 单引号 + TEXT['"'] = """.toCharArray(); // 双引号 + TEXT['&'] = "&".toCharArray(); // &符 + TEXT['<'] = "<".toCharArray(); // 小于号 + TEXT['>'] = ">".toCharArray(); // 大于号 + } + + /** + * 转义文本中的HTML字符为安全的字符 + * + * @param text 被转义的文本 + * @return 转义后的文本 + */ + public static String escape(String text) + { + return encode(text); + } + + /** + * 还原被转义的HTML特殊字符 + * + * @param content 包含转义符的HTML内容 + * @return 转换后的字符串 + */ + public static String unescape(String content) + { + return decode(content); + } + + /** + * 清除所有HTML标签,但是不删除标签内的内容 + * + * @param content 文本 + * @return 清除标签后的文本 + */ + public static String clean(String content) + { + return new HTMLFilter().filter(content); + } + + /** + * Escape编码 + * + * @param text 被编码的文本 + * @return 编码后的字符 + */ + private static String encode(String text) + { + if (StringUtils.isEmpty(text)) + { + return StringUtils.EMPTY; + } + + final StringBuilder tmp = new StringBuilder(text.length() * 6); + char c; + for (int i = 0; i < text.length(); i++) + { + c = text.charAt(i); + if (c < 256) + { + tmp.append("%"); + if (c < 16) + { + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + else + { + tmp.append("%u"); + if (c <= 0xfff) + { + // issue#I49JU8@Gitee + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + } + return tmp.toString(); + } + + /** + * Escape解码 + * + * @param content 被转义的内容 + * @return 解码后的字符串 + */ + public static String decode(String content) + { + if (StringUtils.isEmpty(content)) + { + return content; + } + + StringBuilder tmp = new StringBuilder(content.length()); + int lastPos = 0, pos = 0; + char ch; + while (lastPos < content.length()) + { + pos = content.indexOf("%", lastPos); + if (pos == lastPos) + { + if (content.charAt(pos + 1) == 'u') + { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } + else + { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } + } + else + { + if (pos == -1) + { + tmp.append(content.substring(lastPos)); + lastPos = content.length(); + } + else + { + tmp.append(content.substring(lastPos, pos)); + lastPos = pos; + } + } + } + return tmp.toString(); + } + + public static void main(String[] args) + { + String html = ""; + String escape = EscapeUtil.escape(html); + // String html = "ipt>alert(\"XSS\")ipt>"; + // String html = "<123"; + // String html = "123>"; + System.out.println("clean: " + EscapeUtil.clean(html)); + System.out.println("escape: " + escape); + System.out.println("unescape: " + EscapeUtil.unescape(escape)); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/html/HTMLFilter.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/html/HTMLFilter.java new file mode 100644 index 0000000..1c2e434 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/html/HTMLFilter.java @@ -0,0 +1,570 @@ +package com.storm.common.core.utils.html; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML过滤器,用于去除XSS漏洞隐患。 + * + * @author ruoyi + */ +public final class HTMLFilter +{ + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap<>(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" + * becomes " text "). If set to false, unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() + { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[] { "img" }; + vNeedClosingTags = new String[] { "a", "b", "strong", "i", "em" }; + vDisallowed = new String[] {}; + vAllowedProtocols = new String[] { "http", "mailto", "https" }; // no ftp. + vProtocolAtts = new String[] { "src", "href" }; + vRemoveBlanks = new String[] { "a", "b", "strong", "i", "em" }; + vAllowedEntities = new String[] { "amp", "gt", "lt", "quot" }; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilter(final Map conf) + { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() + { + vTagCounts.clear(); + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) + { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) + { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) + { + reset(); + String s = input; + + s = escapeComments(s); + + s = balanceHTML(s); + + s = checkTags(s); + + s = processRemoveBlanks(s); + + // s = validateEntities(s); + + return s; + } + + public boolean isAlwaysMakeTags() + { + return alwaysMakeTags; + } + + public boolean isStripComments() + { + return stripComment; + } + + private String escapeComments(final String s) + { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) + { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) + { + if (alwaysMakeTags) + { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + // 不追加结束标签 + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } + else + { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) + { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) + { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + final StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) + { + for (int ii = 0; ii < vTagCounts.get(key); ii++) + { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) + { + String result = s; + for (String tag : vRemoveBlanks) + { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) + { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) + { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) + { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) + { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) + { + if (!inArray(name, vSelfClosingTags)) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) + { + final StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) + { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) + { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) + { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) + { + if (inArray(paramName, vProtocolAtts)) + { + paramValue = processParamProtocol(paramValue); + } + params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\""); + } + } + + if (inArray(name, vSelfClosingTags)) + { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) + { + ending = ""; + } + + if (ending == null || ending.length() < 1) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } + else + { + vTagCounts.put(name, 1); + } + } + else + { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } + else + { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) + { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) + { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) + { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) + { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) + { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) + { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) + { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) + { + if (encodeQuotes) + { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two) + m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); + } + m.appendTail(buf); + return buf.toString(); + } + else + { + return s; + } + } + + private String checkEntity(final String preamble, final String term) + { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) + { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) + { + for (String item : array) + { + if (item != null && item.equals(s)) + { + return true; + } + } + return false; + } + + private boolean allowed(final String name) + { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) + { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ip/IpUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ip/IpUtils.java new file mode 100644 index 0000000..5ea290a --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/ip/IpUtils.java @@ -0,0 +1,382 @@ +package com.storm.common.core.utils.ip; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import javax.servlet.http.HttpServletRequest; +import com.storm.common.core.utils.ServletUtils; +import com.storm.common.core.utils.StringUtils; + +/** + * 获取IP方法 + * + * @author ruoyi + */ +public class IpUtils +{ + public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)"; + // 匹配 ip + public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")"; + public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))"; + // 匹配网段 + public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")"; + + /** + * 获取客户端IP + * + * @return IP地址 + */ + public static String getIpAddr() + { + return getIpAddr(ServletUtils.getRequest()); + } + + /** + * 获取客户端IP + * + * @param request 请求对象 + * @return IP地址 + */ + public static String getIpAddr(HttpServletRequest request) + { + if (request == null) + { + return "unknown"; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Forwarded-For"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) + { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) + { + if (StringUtils.isNull(addr) || addr.length < 2) + { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) + { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) + { + return true; + } + case SECTION_5: + switch (b1) + { + case SECTION_6: + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) + { + if (text.length() == 0) + { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try + { + long l; + int i; + switch (elements.length) + { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) + { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) + { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) + { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } + catch (NumberFormatException e) + { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() + { + try + { + return InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException e) + { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException e) + { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) + { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) + { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) + { + if (false == isUnknown(subIp)) + { + ip = subIp; + break; + } + } + } + return StringUtils.substring(ip, 0, 255); + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) + { + return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + + /** + * 是否为IP + */ + public static boolean isIP(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP); + } + + /** + * 是否为IP,或 *为间隔的通配符地址 + */ + public static boolean isIpWildCard(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD); + } + + /** + * 检测参数是否在ip通配符里 + */ + public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip) + { + String[] s1 = ipWildCard.split("\\."); + String[] s2 = ip.split("\\."); + boolean isMatchedSeg = true; + for (int i = 0; i < s1.length && !s1[i].equals("*"); i++) + { + if (!s1[i].equals(s2[i])) + { + isMatchedSeg = false; + break; + } + } + return isMatchedSeg; + } + + /** + * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串 + */ + public static boolean isIPSegment(String ipSeg) + { + return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG); + } + + /** + * 判断ip是否在指定网段中 + */ + public static boolean ipIsInNetNoCheck(String iparea, String ip) + { + int idx = iparea.indexOf('-'); + String[] sips = iparea.substring(0, idx).split("\\."); + String[] sipe = iparea.substring(idx + 1).split("\\."); + String[] sipt = ip.split("\\."); + long ips = 0L, ipe = 0L, ipt = 0L; + for (int i = 0; i < 4; ++i) + { + ips = ips << 8 | Integer.parseInt(sips[i]); + ipe = ipe << 8 | Integer.parseInt(sipe[i]); + ipt = ipt << 8 | Integer.parseInt(sipt[i]); + } + if (ips > ipe) + { + long t = ips; + ips = ipe; + ipe = t; + } + return ips <= ipt && ipt <= ipe; + } + + /** + * 校验ip是否符合过滤串规则 + * + * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99` + * @param ip 校验IP地址 + * @return boolean 结果 + */ + public static boolean isMatchedIp(String filter, String ip) + { + if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip)) + { + return false; + } + String[] ips = filter.split(";"); + for (String iStr : ips) + { + if (isIP(iStr) && iStr.equals(ip)) + { + return true; + } + else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip)) + { + return true; + } + else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip)) + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/poi/ExcelHandlerAdapter.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/poi/ExcelHandlerAdapter.java new file mode 100644 index 0000000..18a1e06 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/poi/ExcelHandlerAdapter.java @@ -0,0 +1,24 @@ +package com.storm.common.core.utils.poi; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Workbook; + +/** + * Excel数据格式处理适配器 + * + * @author ruoyi + */ +public interface ExcelHandlerAdapter +{ + /** + * 格式化 + * + * @param value 单元格数据值 + * @param args excel注解args参数组 + * @param cell 单元格对象 + * @param wb 工作簿对象 + * + * @return 处理后的值 + */ + Object format(Object value, String[] args, Cell cell, Workbook wb); +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/poi/ExcelUtil.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/poi/ExcelUtil.java new file mode 100644 index 0000000..8981312 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/poi/ExcelUtil.java @@ -0,0 +1,1626 @@ +package com.storm.common.core.utils.poi; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.DataFormat; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFDataValidation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.annotation.Excel.ColumnType; +import com.storm.common.core.annotation.Excel.Type; +import com.storm.common.core.annotation.Excels; +import com.storm.common.core.exception.UtilException; +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.DateUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.file.FileTypeUtils; +import com.storm.common.core.utils.file.ImageUtils; +import com.storm.common.core.utils.reflect.ReflectUtils; + +/** + * Excel相关处理 + * + * @author ruoyi + */ +public class ExcelUtil +{ + private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); + + public static final String SEPARATOR = ","; + + public static final String FORMULA_REGEX_STR = "=|-|\\+|@"; + + public static final String[] FORMULA_STR = { "=", "-", "+", "@" }; + + /** + * Excel sheet最大行数,默认65536 + */ + public static final int sheetSize = 65536; + + /** + * 工作表名称 + */ + private String sheetName; + + /** + * 导出类型(EXPORT:导出数据;IMPORT:导入模板) + */ + private Type type; + + /** + * 工作薄对象 + */ + private Workbook wb; + + /** + * 工作表对象 + */ + private Sheet sheet; + + /** + * 样式列表 + */ + private Map styles; + + /** + * 导入导出数据列表 + */ + private List list; + + /** + * 注解列表 + */ + private List fields; + + /** + * 当前行号 + */ + private int rownum; + + /** + * 标题 + */ + private String title; + + /** + * 最大高度 + */ + private short maxHeight; + + /** + * 合并后最后行数 + */ + private int subMergedLastRowNum = 0; + + /** + * 合并后开始行数 + */ + private int subMergedFirstRowNum = 1; + + /** + * 对象的子列表方法 + */ + private Method subMethod; + + /** + * 对象的子列表属性 + */ + private List subFields; + + /** + * 统计列表 + */ + private Map statistics = new HashMap(); + + /** + * 实体对象 + */ + public Class clazz; + + /** + * 需要显示列属性 + */ + public String[] includeFields; + + /** + * 需要排除列属性 + */ + public String[] excludeFields; + + public ExcelUtil(Class clazz) + { + this.clazz = clazz; + } + + /** + * 仅在Excel中显示列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + */ + public void showColumn(String... fields) + { + this.includeFields = fields; + } + + /** + * 隐藏Excel中列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + */ + public void hideColumn(String... fields) + { + this.excludeFields = fields; + } + + public void init(List list, String sheetName, String title, Type type) + { + if (list == null) + { + list = new ArrayList(); + } + this.list = list; + this.sheetName = sheetName; + this.type = type; + this.title = title; + createExcelField(); + createWorkbook(); + createTitle(); + createSubHead(); + } + + /** + * 创建excel第一行标题 + */ + public void createTitle() + { + if (StringUtils.isNotEmpty(title)) + { + int titleLastCol = this.fields.size() - 1; + if (isSubList()) + { + titleLastCol = titleLastCol + subFields.size() - 1; + } + Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0); + titleRow.setHeightInPoints(30); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellStyle(styles.get("title")); + titleCell.setCellValue(title); + sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), 0, titleLastCol)); + } + } + + /** + * 创建对象的子列表名称 + */ + public void createSubHead() + { + if (isSubList()) + { + Row subRow = sheet.createRow(rownum); + int column = 0; + int subFieldSize = subFields != null ? subFields.size() : 0; + for (Object[] objects : fields) + { + Field field = (Field) objects[0]; + Excel attr = (Excel) objects[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + Cell cell = subRow.createCell(column); + cell.setCellValue(attr.name()); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + if (subFieldSize > 1) + { + CellRangeAddress cellAddress = new CellRangeAddress(rownum, rownum, column, column + subFieldSize - 1); + sheet.addMergedRegion(cellAddress); + } + column += subFieldSize; + } + else + { + Cell cell = subRow.createCell(column++); + cell.setCellValue(attr.name()); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + } + } + rownum++; + } + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(InputStream is) + { + return importExcel(is, 0); + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @param titleNum 标题占用行数 + * @return 转换后集合 + */ + public List importExcel(InputStream is, int titleNum) + { + List list = null; + try + { + list = importExcel(StringUtils.EMPTY, is, titleNum); + } + catch (Exception e) + { + log.error("导入Excel异常{}", e.getMessage()); + throw new UtilException(e.getMessage()); + } + finally + { + IOUtils.closeQuietly(is); + } + return list; + } + + /** + * 对excel表单指定表格索引名转换成list + * + * @param sheetName 表格索引名 + * @param titleNum 标题占用行数 + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(String sheetName, InputStream is, int titleNum) throws Exception + { + this.type = Type.IMPORT; + this.wb = WorkbookFactory.create(is); + List list = new ArrayList(); + // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet + Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0); + if (sheet == null) + { + throw new IOException("文件sheet不存在"); + } + + // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1 + int rows = sheet.getLastRowNum(); + if (rows > 0) + { + // 定义一个map用于存放excel列的序号和field. + Map cellMap = new HashMap(); + // 获取表头 + Row heard = sheet.getRow(titleNum); + for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) + { + Cell cell = heard.getCell(i); + if (StringUtils.isNotNull(cell)) + { + String value = this.getCellValue(heard, i).toString(); + cellMap.put(value, i); + } + else + { + cellMap.put(null, i); + } + } + // 有数据时才处理 得到类的所有field. + List fields = this.getFields(); + Map fieldsMap = new HashMap(); + for (Object[] objects : fields) + { + Excel attr = (Excel) objects[1]; + Integer column = cellMap.get(attr.name()); + if (column != null) + { + fieldsMap.put(column, objects); + } + } + for (int i = titleNum + 1; i <= rows; i++) + { + // 从第2行开始取数据,默认第一行是表头. + Row row = sheet.getRow(i); + // 判断当前行是否是空行 + if (isRowEmpty(row)) + { + continue; + } + T entity = null; + for (Map.Entry entry : fieldsMap.entrySet()) + { + Object val = this.getCellValue(row, entry.getKey()); + + // 如果不存在实例则新建. + entity = (entity == null ? clazz.newInstance() : entity); + // 从map中得到对应列的field. + Field field = (Field) entry.getValue()[0]; + Excel attr = (Excel) entry.getValue()[1]; + // 取得类型,并根据对象类型设置值. + Class fieldType = field.getType(); + if (String.class == fieldType) + { + String s = Convert.toStr(val); + if (s.matches("^\\d+\\.0$")) + { + val = StringUtils.substringBefore(s, ".0"); + } + else + { + String dateFormat = field.getAnnotation(Excel.class).dateFormat(); + if (StringUtils.isNotEmpty(dateFormat)) + { + val = parseDateToStr(dateFormat, val); + } + else + { + val = Convert.toStr(val); + } + } + } + else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toInt(val); + } + else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toLong(val); + } + else if (Double.TYPE == fieldType || Double.class == fieldType) + { + val = Convert.toDouble(val); + } + else if (Float.TYPE == fieldType || Float.class == fieldType) + { + val = Convert.toFloat(val); + } + else if (BigDecimal.class == fieldType) + { + val = Convert.toBigDecimal(val); + } + else if (Date.class == fieldType) + { + if (val instanceof String) + { + val = DateUtils.parseDate(val); + } + else if (val instanceof Double) + { + val = DateUtil.getJavaDate((Double) val); + } + } + else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) + { + val = Convert.toBool(val, false); + } + if (StringUtils.isNotNull(fieldType)) + { + String propertyName = field.getName(); + if (StringUtils.isNotEmpty(attr.targetAttr())) + { + propertyName = field.getName() + "." + attr.targetAttr(); + } + if (StringUtils.isNotEmpty(attr.readConverterExp())) + { + val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator()); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + val = dataFormatHandlerAdapter(val, attr, null); + } + ReflectUtils.invokeSetter(entity, propertyName, val); + } + } + list.add(entity); + } + } + return list; + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName) + { + exportExcel(response, list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(list, sheetName, title, Type.EXPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName) + { + importTemplateExcel(response, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(null, sheetName, title, Type.IMPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public void exportExcel(HttpServletResponse response) + { + try + { + writeSheet(); + wb.write(response.getOutputStream()); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + } + finally + { + IOUtils.closeQuietly(wb); + } + } + + /** + * 创建写入数据到Sheet + */ + public void writeSheet() + { + // 取出一共有多少个sheet. + int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize)); + for (int index = 0; index < sheetNo; index++) + { + createSheet(sheetNo, index); + + // 产生一行 + Row row = sheet.createRow(rownum); + int column = 0; + // 写入各个字段的列头名称 + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + this.createHeadCell(subExcel, row, column++); + } + } + else + { + this.createHeadCell(excel, row, column++); + } + } + if (Type.EXPORT.equals(type)) + { + fillExcelData(index, row); + addStatisticsRow(); + } + } + } + + /** + * 填充excel数据 + * + * @param index 序号 + * @param row 单元格行 + */ + @SuppressWarnings("unchecked") + public void fillExcelData(int index, Row row) + { + int startNo = index * sheetSize; + int endNo = Math.min(startNo + sheetSize, list.size()); + int currentRowNum = rownum + 1; // 从标题行后开始 + + for (int i = startNo; i < endNo; i++) + { + row = sheet.createRow(currentRowNum); + T vo = (T) list.get(i); + int column = 0; + int maxSubListSize = getCurrentMaxSubListSize(vo); + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + try + { + Collection subList = (Collection) getTargetValue(vo, field, excel); + if (subList != null && !subList.isEmpty()) + { + int subIndex = 0; + for (Object subVo : subList) + { + Row subRow = sheet.getRow(currentRowNum + subIndex); + if (subRow == null) + { + subRow = sheet.createRow(currentRowNum + subIndex); + } + + int subColumn = column; + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + addCell(subExcel, subRow, (T) subVo, subField, subColumn++); + } + subIndex++; + } + column += subFields.size(); + } + } + catch (Exception e) + { + log.error("填充集合数据失败", e); + } + } + else + { + // 创建单元格并设置值 + addCell(excel, row, vo, field, column); + if (maxSubListSize > 1 && excel.needMerge()) + { + sheet.addMergedRegion(new CellRangeAddress(currentRowNum, currentRowNum + maxSubListSize - 1, column, column)); + } + column++; + } + } + currentRowNum += maxSubListSize; + } + } + + /** + * 获取子列表最大数 + */ + private int getCurrentMaxSubListSize(T vo) + { + int maxSubListSize = 1; + for (Object[] os : fields) + { + Field field = (Field) os[0]; + if (Collection.class.isAssignableFrom(field.getType())) + { + try + { + Collection subList = (Collection) getTargetValue(vo, field, (Excel) os[1]); + if (subList != null && !subList.isEmpty()) + { + maxSubListSize = Math.max(maxSubListSize, subList.size()); + } + } + catch (Exception e) + { + log.error("获取集合大小失败", e); + } + } + } + return maxSubListSize; + } + + /** + * 创建表格样式 + * + * @param wb 工作薄对象 + * @return 样式列表 + */ + private Map createStyles(Workbook wb) + { + // 写入各条记录,每条记录对应excel表中的一行 + Map styles = new HashMap(); + CellStyle style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font titleFont = wb.createFont(); + titleFont.setFontName("Arial"); + titleFont.setFontHeightInPoints((short) 16); + titleFont.setBold(true); + style.setFont(titleFont); + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + styles.put("title", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + style.setFont(dataFont); + styles.put("data", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setDataFormat(dataFormat.getFormat("######0.00")); + Font totalFont = wb.createFont(); + totalFont.setFontName("Arial"); + totalFont.setFontHeightInPoints((short) 10); + style.setFont(totalFont); + styles.put("total", style); + + styles.putAll(annotationHeaderStyles(wb, styles)); + + styles.putAll(annotationDataStyles(wb)); + + return styles; + } + + /** + * 根据Excel注解创建表格头样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationHeaderStyles(Workbook wb, Map styles) + { + Map headerStyles = new HashMap(); + for (Object[] os : fields) + { + Excel excel = (Excel) os[1]; + String key = StringUtils.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor()); + if (!headerStyles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setFillForegroundColor(excel.headerBackgroundColor().index); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setBold(true); + headerFont.setColor(excel.headerColor().index); + style.setFont(headerFont); + // 设置表格头单元格文本形式 + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + headerStyles.put(key, style); + } + } + return headerStyles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationDataStyles(Workbook wb) + { + Map styles = new HashMap(); + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + List subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class); + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + annotationDataStyles(styles, subField, subExcel); + } + } + else + { + annotationDataStyles(styles, field, excel); + } + } + return styles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param styles 自定义样式列表 + * @param field 属性列信息 + * @param excel 注解信息 + */ + public void annotationDataStyles(Map styles, Field field, Excel excel) + { + String key = StringUtils.format("data_{}_{}_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor(), excel.cellType(), excel.wrapText()); + if (!styles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.setAlignment(excel.align()); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setFillForegroundColor(excel.backgroundColor().getIndex()); + style.setWrapText(excel.wrapText()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + dataFont.setColor(excel.color().index); + style.setFont(dataFont); + if (ColumnType.TEXT == excel.cellType()) + { + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + } + styles.put(key, style); + } + } + + /** + * 创建单元格 + */ + public Cell createHeadCell(Excel attr, Row row, int column) + { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(attr.name()); + setDataValidation(attr, row, column); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + if (isSubList()) + { + // 填充默认样式,防止合并单元格样式失效 + sheet.setDefaultColumnStyle(column, styles.get(StringUtils.format("data_{}_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType(), attr.wrapText()))); + if (attr.needMerge()) + { + sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, column, column)); + } + } + return cell; + } + + /** + * 设置单元格信息 + * + * @param value 单元格值 + * @param attr 注解相关 + * @param cell 单元格信息 + */ + public void setCellVo(Object value, Excel attr, Cell cell) + { + if (ColumnType.STRING == attr.cellType() || ColumnType.TEXT == attr.cellType()) + { + String cellValue = Convert.toStr(value); + // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。 + if (StringUtils.startsWithAny(cellValue, FORMULA_STR)) + { + cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0"); + } + if (value instanceof Collection && StringUtils.equals("[]", cellValue)) + { + cellValue = StringUtils.EMPTY; + } + cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix()); + } + else if (ColumnType.NUMERIC == attr.cellType()) + { + if (StringUtils.isNotNull(value)) + { + cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value)); + } + } + else if (ColumnType.IMAGE == attr.cellType()) + { + ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1); + String imagePath = Convert.toStr(value); + if (StringUtils.isNotEmpty(imagePath)) + { + byte[] data = ImageUtils.getImage(imagePath); + getDrawingPatriarch(cell.getSheet()).createPicture(anchor, + cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + } + } + } + + /** + * 获取画布 + */ + public static Drawing getDrawingPatriarch(Sheet sheet) + { + if (sheet.getDrawingPatriarch() == null) + { + sheet.createDrawingPatriarch(); + } + return sheet.getDrawingPatriarch(); + } + + /** + * 获取图片类型,设置图片插入类型 + */ + public int getImageType(byte[] value) + { + String type = FileTypeUtils.getFileExtendName(value); + if ("JPG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_JPEG; + } + else if ("PNG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_PNG; + } + return Workbook.PICTURE_TYPE_JPEG; + } + + /** + * 创建表格样式 + */ + public void setDataValidation(Excel attr, Row row, int column) + { + if (attr.name().indexOf("注:") >= 0) + { + sheet.setColumnWidth(column, 6000); + } + else + { + // 设置列宽 + sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256)); + } + if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0) + { + if (attr.combo().length > 15 || StringUtils.join(attr.combo()).length() > 255) + { + // 如果下拉数大于15或字符串长度大于255,则使用一个新sheet存储,避免生成的模板下拉值获取不到 + setXSSFValidationWithHidden(sheet, attr.combo(), attr.prompt(), 1, 100, column, column); + } + else + { + // 提示信息或只能选择不能输入的列内容. + setPromptOrValidation(sheet, attr.combo(), attr.prompt(), 1, 100, column, column); + } + } + } + + /** + * 添加单元格 + */ + public Cell addCell(Excel attr, Row row, T vo, Field field, int column) + { + Cell cell = null; + try + { + // 设置行高 + row.setHeight(maxHeight); + // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列. + if (attr.isExport()) + { + // 创建cell + cell = row.createCell(column); + if (isSubListValue(vo) && getListCellValue(vo).size() > 1 && attr.needMerge()) + { + if (subMergedLastRowNum >= subMergedFirstRowNum) + { + sheet.addMergedRegion(new CellRangeAddress(subMergedFirstRowNum, subMergedLastRowNum, column, column)); + } + } + cell.setCellStyle(styles.get(StringUtils.format("data_{}_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType(), attr.wrapText()))); + + // 用于读取对象中的属性 + Object value = getTargetValue(vo, field, attr); + String dateFormat = attr.dateFormat(); + String readConverterExp = attr.readConverterExp(); + String separator = attr.separator(); + if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) + { + cell.getCellStyle().setDataFormat(this.wb.getCreationHelper().createDataFormat().getFormat(dateFormat)); + cell.setCellValue(parseDateToStr(dateFormat, value)); + } + else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) + { + cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator)); + } + else if (value instanceof BigDecimal && -1 != attr.scale()) + { + cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue()); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + cell.setCellValue(dataFormatHandlerAdapter(value, attr, cell)); + } + else + { + // 设置列类型 + setCellVo(value, attr, cell); + } + addStatisticsData(column, Convert.toStr(value), attr); + } + } + catch (Exception e) + { + log.error("导出Excel失败{}", e); + } + return cell; + } + + /** + * 设置 POI XSSFSheet 单元格提示或选择框 + * + * @param sheet 表单 + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setPromptOrValidation(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, + int firstCol, int endCol) + { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1"); + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + sheet.addValidationData(dataValidation); + } + + /** + * 设置某些列的值只能输入预制的数据,显示下拉框(兼容超出一定数量的下拉框). + * + * @param sheet 要设置的sheet. + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setXSSFValidationWithHidden(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, int firstCol, int endCol) + { + String hideSheetName = "combo_" + firstCol + "_" + endCol; + Sheet hideSheet = wb.createSheet(hideSheetName); // 用于存储 下拉菜单数据 + for (int i = 0; i < textlist.length; i++) + { + hideSheet.createRow(i).createCell(0).setCellValue(textlist[i]); + } + // 创建名称,可被其他单元格引用 + Name name = wb.createName(); + name.setNameName(hideSheetName + "_data"); + name.setRefersToFormula(hideSheetName + "!$A$1:$A$" + textlist.length); + DataValidationHelper helper = sheet.getDataValidationHelper(); + // 加载下拉列表内容 + DataValidationConstraint constraint = helper.createFormulaListConstraint(hideSheetName + "_data"); + // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + // 数据有效性对象 + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + + sheet.addValidationData(dataValidation); + // 设置hiddenSheet隐藏 + wb.setSheetHidden(wb.getSheetIndex(hideSheet), true); + } + + /** + * 解析导出值 0=男,1=女,2=未知 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String convertByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(SEPARATOR); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[0].equals(value)) + { + propertyString.append(itemArray[1] + separator); + break; + } + } + } + else + { + if (itemArray[0].equals(propertyValue)) + { + return itemArray[1]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 反向解析值 男=0,女=1,未知=2 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String reverseByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(SEPARATOR); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[1].equals(value)) + { + propertyString.append(itemArray[0] + separator); + break; + } + } + } + else + { + if (itemArray[1].equals(propertyValue)) + { + return itemArray[0]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 数据处理器 + * + * @param value 数据值 + * @param excel 数据注解 + * @return + */ + public String dataFormatHandlerAdapter(Object value, Excel excel, Cell cell) + { + try + { + Object instance = excel.handler().newInstance(); + Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class, Cell.class, Workbook.class }); + value = formatMethod.invoke(instance, value, excel.args(), cell, this.wb); + } + catch (Exception e) + { + log.error("不能格式化数据 " + excel.handler(), e.getMessage()); + } + return Convert.toStr(value); + } + + /** + * 合计统计信息 + */ + private void addStatisticsData(Integer index, String text, Excel entity) + { + if (entity != null && entity.isStatistics()) + { + Double temp = 0D; + if (!statistics.containsKey(index)) + { + statistics.put(index, temp); + } + try + { + temp = Double.valueOf(text); + } + catch (NumberFormatException e) + { + } + statistics.put(index, statistics.get(index) + temp); + } + } + + /** + * 创建统计行 + */ + public void addStatisticsRow() + { + if (statistics.size() > 0) + { + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + Set keys = statistics.keySet(); + Cell cell = row.createCell(0); + cell.setCellStyle(styles.get("total")); + cell.setCellValue("合计"); + + for (Integer key : keys) + { + cell = row.createCell(key); + cell.setCellStyle(styles.get("total")); + cell.setCellValue(statistics.get(key)); + } + statistics.clear(); + } + } + + /** + * 获取bean中的属性值 + * + * @param vo 实体对象 + * @param field 字段 + * @param excel 注解 + * @return 最终的属性值 + * @throws Exception + */ + private Object getTargetValue(T vo, Field field, Excel excel) throws Exception + { + field.setAccessible(true); + Object o = field.get(vo); + if (StringUtils.isNotEmpty(excel.targetAttr())) + { + String target = excel.targetAttr(); + if (target.contains(".")) + { + String[] targets = target.split("[.]"); + for (String name : targets) + { + o = getValue(o, name); + } + } + else + { + o = getValue(o, target); + } + } + return o; + } + + /** + * 以类的属性的get方法方法形式获取值 + * + * @param o + * @param name + * @return value + * @throws Exception + */ + private Object getValue(Object o, String name) throws Exception + { + if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) + { + Class clazz = o.getClass(); + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + o = field.get(o); + } + return o; + } + + /** + * 得到所有定义字段 + */ + private void createExcelField() + { + this.fields = getFields(); + this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList()); + this.maxHeight = getRowHeight(); + } + + /** + * 获取字段注解信息 + */ + public List getFields() + { + List fields = new ArrayList(); + List tempFields = new ArrayList<>(); + tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); + tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + if (StringUtils.isNotEmpty(includeFields)) + { + for (Field field : tempFields) + { + if (ArrayUtils.contains(this.includeFields, field.getName()) || field.isAnnotationPresent(Excels.class)) + { + addField(fields, field); + } + } + } + else if (StringUtils.isNotEmpty(excludeFields)) + { + for (Field field : tempFields) + { + if (!ArrayUtils.contains(this.excludeFields, field.getName())) + { + addField(fields, field); + } + } + } + else + { + for (Field field : tempFields) + { + addField(fields, field); + } + } + return fields; + } + + /** + * 添加字段信息 + */ + public void addField(List fields, Field field) + { + // 单注解 + if (field.isAnnotationPresent(Excel.class)) + { + Excel attr = field.getAnnotation(Excel.class); + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) + { + fields.add(new Object[] { field, attr }); + } + if (Collection.class.isAssignableFrom(field.getType())) + { + subMethod = getSubMethod(field.getName(), clazz); + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class); + } + } + + // 多注解 + if (field.isAnnotationPresent(Excels.class)) + { + Excels attrs = field.getAnnotation(Excels.class); + Excel[] excels = attrs.value(); + for (Excel attr : excels) + { + if (StringUtils.isNotEmpty(includeFields)) + { + if (ArrayUtils.contains(this.includeFields, field.getName() + "." + attr.targetAttr()) + && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) + { + fields.add(new Object[] { field, attr }); + } + } + else + { + if (!ArrayUtils.contains(this.excludeFields, field.getName() + "." + attr.targetAttr()) + && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) + { + fields.add(new Object[] { field, attr }); + } + } + } + } + } + + /** + * 根据注解获取最大行高 + */ + public short getRowHeight() + { + double maxHeight = 0; + for (Object[] os : this.fields) + { + Excel excel = (Excel) os[1]; + maxHeight = Math.max(maxHeight, excel.height()); + } + return (short) (maxHeight * 20); + } + + /** + * 创建一个工作簿 + */ + public void createWorkbook() + { + this.wb = new SXSSFWorkbook(500); + this.sheet = wb.createSheet(); + wb.setSheetName(0, sheetName); + this.styles = createStyles(wb); + } + + /** + * 创建工作表 + * + * @param sheetNo sheet数量 + * @param index 序号 + */ + public void createSheet(int sheetNo, int index) + { + // 设置工作表的名称. + if (sheetNo > 1 && index > 0) + { + this.sheet = wb.createSheet(); + this.createTitle(); + wb.setSheetName(index, sheetName + index); + } + } + + /** + * 获取单元格值 + * + * @param row 获取的行 + * @param column 获取单元格列号 + * @return 单元格值 + */ + public Object getCellValue(Row row, int column) + { + if (row == null) + { + return row; + } + Object val = ""; + try + { + Cell cell = row.getCell(column); + if (StringUtils.isNotNull(cell)) + { + if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) + { + val = cell.getNumericCellValue(); + if (DateUtil.isCellDateFormatted(cell)) + { + val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换 + } + else + { + if ((Double) val % 1 != 0) + { + val = new BigDecimal(val.toString()); + } + else + { + val = new DecimalFormat("0").format(val); + } + } + } + else if (cell.getCellType() == CellType.STRING) + { + val = cell.getStringCellValue(); + } + else if (cell.getCellType() == CellType.BOOLEAN) + { + val = cell.getBooleanCellValue(); + } + else if (cell.getCellType() == CellType.ERROR) + { + val = cell.getErrorCellValue(); + } + + } + } + catch (Exception e) + { + return val; + } + return val; + } + + /** + * 判断是否是空行 + * + * @param row 判断的行 + * @return + */ + private boolean isRowEmpty(Row row) + { + if (row == null) + { + return true; + } + for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) + { + Cell cell = row.getCell(i); + if (cell != null && cell.getCellType() != CellType.BLANK) + { + return false; + } + } + return true; + } + + /** + * 格式化不同类型的日期对象 + * + * @param dateFormat 日期格式 + * @param val 被格式化的日期对象 + * @return 格式化后的日期字符 + */ + public String parseDateToStr(String dateFormat, Object val) + { + if (val == null) + { + return ""; + } + String str; + if (val instanceof Date) + { + str = DateUtils.parseDateToStr(dateFormat, (Date) val); + } + else if (val instanceof LocalDateTime) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDateTime) val)); + } + else if (val instanceof LocalDate) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDate) val)); + } + else + { + str = val.toString(); + } + return str; + } + + /** + * 是否有对象的子列表 + */ + public boolean isSubList() + { + return StringUtils.isNotNull(subFields) && subFields.size() > 0; + } + + /** + * 是否有对象的子列表,集合不为空 + */ + public boolean isSubListValue(T vo) + { + return StringUtils.isNotNull(subFields) && subFields.size() > 0 && StringUtils.isNotNull(getListCellValue(vo)) && getListCellValue(vo).size() > 0; + } + + /** + * 获取集合的值 + */ + public Collection getListCellValue(Object obj) + { + Object value; + try + { + value = subMethod.invoke(obj, new Object[] {}); + } + catch (Exception e) + { + return new ArrayList(); + } + return (Collection) value; + } + + /** + * 获取对象的子列表方法 + * + * @param name 名称 + * @param pojoClass 类对象 + * @return 子列表方法 + */ + public Method getSubMethod(String name, Class pojoClass) + { + StringBuffer getMethodName = new StringBuffer("get"); + getMethodName.append(name.substring(0, 1).toUpperCase()); + getMethodName.append(name.substring(1)); + Method method = null; + try + { + method = pojoClass.getMethod(getMethodName.toString(), new Class[] {}); + } + catch (Exception e) + { + log.error("获取对象异常{}", e.getMessage()); + } + return method; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/reflect/ReflectUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/reflect/ReflectUtils.java new file mode 100644 index 0000000..3dda04b --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/reflect/ReflectUtils.java @@ -0,0 +1,410 @@ +package com.storm.common.core.utils.reflect; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Date; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.poi.ss.usermodel.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.DateUtils; + +/** + * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数. + * + * @author ruoyi + */ +@SuppressWarnings("rawtypes") +public class ReflectUtils +{ + private static final String SETTER_PREFIX = "set"; + + private static final String GETTER_PREFIX = "get"; + + private static final String CGLIB_CLASS_SEPARATOR = "$$"; + + private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class); + + /** + * 调用Getter方法. + * 支持多级,如:对象名.对象名.方法 + */ + @SuppressWarnings("unchecked") + public static E invokeGetter(Object obj, String propertyName) + { + Object object = obj; + for (String name : StringUtils.split(propertyName, ".")) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + return (E) object; + } + + /** + * 调用Setter方法, 仅匹配方法名。 + * 支持多级,如:对象名.对象名.方法 + */ + public static void invokeSetter(Object obj, String propertyName, E value) + { + Object object = obj; + String[] names = StringUtils.split(propertyName, "."); + for (int i = 0; i < names.length; i++) + { + if (i < names.length - 1) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + else + { + String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]); + invokeMethodByName(object, setterMethodName, new Object[] { value }); + } + } + } + + /** + * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数. + */ + @SuppressWarnings("unchecked") + public static E getFieldValue(final Object obj, final String fieldName) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return null; + } + E result = null; + try + { + result = (E) field.get(obj); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常{}", e.getMessage()); + } + return result; + } + + /** + * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数. + */ + public static void setFieldValue(final Object obj, final String fieldName, final E value) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return; + } + try + { + field.set(obj, value); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常: {}", e.getMessage()); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符. + * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用. + * 同时匹配方法名+参数类型, + */ + @SuppressWarnings("unchecked") + public static E invokeMethod(final Object obj, final String methodName, final Class[] parameterTypes, + final Object[] args) + { + if (obj == null || methodName == null) + { + return null; + } + Method method = getAccessibleMethod(obj, methodName, parameterTypes); + if (method == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符, + * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用. + * 只匹配函数名,如果有多个同名函数调用第一个。 + */ + @SuppressWarnings("unchecked") + public static E invokeMethodByName(final Object obj, final String methodName, final Object[] args) + { + Method method = getAccessibleMethodByName(obj, methodName, args.length); + if (method == null) + { + // 如果为空不报错,直接返回空。 + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + // 类型转换(将参数数据类型转换为目标方法参数类型) + Class[] cs = method.getParameterTypes(); + for (int i = 0; i < cs.length; i++) + { + if (args[i] != null && !args[i].getClass().equals(cs[i])) + { + if (cs[i] == String.class) + { + args[i] = Convert.toStr(args[i]); + if (StringUtils.endsWith((String) args[i], ".0")) + { + args[i] = StringUtils.substringBefore((String) args[i], ".0"); + } + } + else if (cs[i] == Integer.class) + { + args[i] = Convert.toInt(args[i]); + } + else if (cs[i] == Long.class) + { + args[i] = Convert.toLong(args[i]); + } + else if (cs[i] == Double.class) + { + args[i] = Convert.toDouble(args[i]); + } + else if (cs[i] == Float.class) + { + args[i] = Convert.toFloat(args[i]); + } + else if (cs[i] == Date.class) + { + if (args[i] instanceof String) + { + args[i] = DateUtils.parseDate(args[i]); + } + else + { + args[i] = DateUtil.getJavaDate((Double) args[i]); + } + } + else if (cs[i] == boolean.class || cs[i] == Boolean.class) + { + args[i] = Convert.toBool(args[i]); + } + } + } + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + */ + public static Field getAccessibleField(final Object obj, final String fieldName) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(fieldName, "fieldName can't be blank"); + for (Class superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) + { + try + { + Field field = superClass.getDeclaredField(fieldName); + makeAccessible(field); + return field; + } + catch (NoSuchFieldException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 匹配函数名+参数类型。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethod(final Object obj, final String methodName, + final Class... parameterTypes) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + try + { + Method method = searchType.getDeclaredMethod(methodName, parameterTypes); + makeAccessible(method); + return method; + } + catch (NoSuchMethodException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 只匹配函数名。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethodByName(final Object obj, final String methodName, int argsNum) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + Method[] methods = searchType.getDeclaredMethods(); + for (Method method : methods) + { + if (method.getName().equals(methodName) && method.getParameterTypes().length == argsNum) + { + makeAccessible(method); + return method; + } + } + } + return null; + } + + /** + * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Method method) + { + if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) + && !method.isAccessible()) + { + method.setAccessible(true); + } + } + + /** + * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Field field) + { + if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) + || Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) + { + field.setAccessible(true); + } + } + + /** + * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处 + * 如无法找到, 返回Object.class. + */ + @SuppressWarnings("unchecked") + public static Class getClassGenricType(final Class clazz) + { + return getClassGenricType(clazz, 0); + } + + /** + * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. + * 如无法找到, 返回Object.class. + */ + public static Class getClassGenricType(final Class clazz, final int index) + { + Type genType = clazz.getGenericSuperclass(); + + if (!(genType instanceof ParameterizedType)) + { + logger.debug(clazz.getSimpleName() + "'s superclass not ParameterizedType"); + return Object.class; + } + + Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); + + if (index >= params.length || index < 0) + { + logger.debug("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + + params.length); + return Object.class; + } + if (!(params[index] instanceof Class)) + { + logger.debug(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); + return Object.class; + } + + return (Class) params[index]; + } + + public static Class getUserClass(Object instance) + { + if (instance == null) + { + throw new RuntimeException("Instance must not be null"); + } + Class clazz = instance.getClass(); + if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) + { + Class superClass = clazz.getSuperclass(); + if (superClass != null && !Object.class.equals(superClass)) + { + return superClass; + } + } + return clazz; + + } + + /** + * 将反射时的checked exception转换为unchecked exception. + */ + public static RuntimeException convertReflectionExceptionToUnchecked(String msg, Exception e) + { + if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException + || e instanceof NoSuchMethodException) + { + return new IllegalArgumentException(msg, e); + } + else if (e instanceof InvocationTargetException) + { + return new RuntimeException(msg, ((InvocationTargetException) e).getTargetException()); + } + return new RuntimeException(msg, e); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/sign/Base64.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/sign/Base64.java new file mode 100644 index 0000000..9b7e624 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/sign/Base64.java @@ -0,0 +1,291 @@ +package com.storm.common.core.utils.sign; + +/** + * Base64工具类 + * + * @author ruoyi + */ +public final class Base64 +{ + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static final private byte[] base64Alphabet = new byte[BASELENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static + { + for (int i = 0; i < BASELENGTH; ++i) + { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) + { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) + { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) + { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) + { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + } + + private static boolean isWhiteSpace(char octect) + { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) + { + return (octect == PAD); + } + + private static boolean isData(char octect) + { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) + { + if (binaryData == null) + { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) + { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) + { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) + { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } + else if (fewerThan24bits == SIXTEENBIT) + { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) + { + if (encoded == null) + { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) + { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) + { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) + { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) + { + return null; + } // if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) + { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) + {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) + { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } + else if (!isPad(d3) && isPad(d4)) + { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } + else + { + return null; + } + } + else + { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) + { + if (data == null) + { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) + { + if (!isWhiteSpace(data[i])) + { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/sql/SqlUtil.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/sql/SqlUtil.java new file mode 100644 index 0000000..4f25fb0 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/sql/SqlUtil.java @@ -0,0 +1,70 @@ +package com.storm.common.core.utils.sql; + +import com.storm.common.core.exception.UtilException; +import com.storm.common.core.utils.StringUtils; + +/** + * sql操作工具类 + * + * @author ruoyi + */ +public class SqlUtil +{ + /** + * 定义常用的 sql关键字 + */ + public static String SQL_REGEX = "\u000B|and |extractvalue|updatexml|sleep|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()"; + + /** + * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序) + */ + public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; + + /** + * 限制orderBy最大长度 + */ + private static final int ORDER_BY_MAX_LENGTH = 500; + + /** + * 检查字符,防止注入绕过 + */ + public static String escapeOrderBySql(String value) + { + if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) + { + throw new UtilException("参数不符合规范,不能进行查询"); + } + if (StringUtils.length(value) > ORDER_BY_MAX_LENGTH) + { + throw new UtilException("参数已超过最大限制,不能进行查询"); + } + return value; + } + + /** + * 验证 order by 语法是否符合规范 + */ + public static boolean isValidOrderBySql(String value) + { + return value.matches(SQL_PATTERN); + } + + /** + * SQL关键字检查 + */ + public static void filterKeyword(String value) + { + if (StringUtils.isEmpty(value)) + { + return; + } + String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|"); + for (String sqlKeyword : sqlKeywords) + { + if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) + { + throw new UtilException("参数存在SQL注入风险"); + } + } + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/uuid/IdUtils.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/uuid/IdUtils.java new file mode 100644 index 0000000..0f41845 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/uuid/IdUtils.java @@ -0,0 +1,49 @@ +package com.storm.common.core.utils.uuid; + +/** + * ID生成器工具类 + * + * @author ruoyi + */ +public class IdUtils +{ + /** + * 获取随机UUID + * + * @return 随机UUID + */ + public static String randomUUID() + { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + */ + public static String simpleUUID() + { + return UUID.randomUUID().toString(true); + } + + /** + * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 随机UUID + */ + public static String fastUUID() + { + return UUID.fastUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 简化的UUID,去掉了横线 + */ + public static String fastSimpleUUID() + { + return UUID.fastUUID().toString(true); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/uuid/Seq.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/uuid/Seq.java new file mode 100644 index 0000000..922d551 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/uuid/Seq.java @@ -0,0 +1,86 @@ +package com.storm.common.core.utils.uuid; + +import java.util.concurrent.atomic.AtomicInteger; +import com.storm.common.core.utils.DateUtils; +import com.storm.common.core.utils.StringUtils; + +/** + * @author ruoyi 序列生成类 + */ +public class Seq +{ + // 通用序列类型 + public static final String commSeqType = "COMMON"; + + // 上传序列类型 + public static final String uploadSeqType = "UPLOAD"; + + // 通用接口序列数 + private static AtomicInteger commSeq = new AtomicInteger(1); + + // 上传接口序列数 + private static AtomicInteger uploadSeq = new AtomicInteger(1); + + // 机器标识 + private static final String machineCode = "A"; + + /** + * 获取通用序列号 + * + * @return 序列值 + */ + public static String getId() + { + return getId(commSeqType); + } + + /** + * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串 + * + * @return 序列值 + */ + public static String getId(String type) + { + AtomicInteger atomicInt = commSeq; + if (uploadSeqType.equals(type)) + { + atomicInt = uploadSeq; + } + return getId(atomicInt, 3); + } + + /** + * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串 + * + * @param atomicInt 序列数 + * @param length 数值长度 + * @return 序列值 + */ + public static String getId(AtomicInteger atomicInt, int length) + { + String result = DateUtils.dateTimeNow(); + result += machineCode; + result += getSeq(atomicInt, length); + return result; + } + + /** + * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数 + * + * @return 序列值 + */ + private synchronized static String getSeq(AtomicInteger atomicInt, int length) + { + // 先取值再+1 + int value = atomicInt.getAndIncrement(); + + // 如果更新后值>=10 的 (length)幂次方则重置为1 + int maxSeq = (int) Math.pow(10, length); + if (atomicInt.get() >= maxSeq) + { + atomicInt.set(1); + } + // 转字符串,用0左补齐 + return StringUtils.padl(value, length); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/uuid/UUID.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/uuid/UUID.java new file mode 100644 index 0000000..b942896 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/utils/uuid/UUID.java @@ -0,0 +1,484 @@ +package com.storm.common.core.utils.uuid; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import com.storm.common.core.exception.UtilException; + +/** + * 提供通用唯一识别码(universally unique identifier)(UUID)实现 + * + * @author ruoyi + */ +public final class UUID implements java.io.Serializable, Comparable +{ + private static final long serialVersionUID = -1185015143654744140L; + + /** + * SecureRandom 的单例 + * + */ + private static class Holder + { + static final SecureRandom numberGenerator = getSecureRandom(); + } + + /** 此UUID的最高64有效位 */ + private final long mostSigBits; + + /** 此UUID的最低64有效位 */ + private final long leastSigBits; + + /** + * 私有构造 + * + * @param data 数据 + */ + private UUID(byte[] data) + { + long msb = 0; + long lsb = 0; + assert data.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) + { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) + { + lsb = (lsb << 8) | (data[i] & 0xff); + } + this.mostSigBits = msb; + this.leastSigBits = lsb; + } + + /** + * 使用指定的数据构造新的 UUID。 + * + * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 + * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 + */ + public UUID(long mostSigBits, long leastSigBits) + { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID fastUUID() + { + return randomUUID(false); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID() + { + return randomUUID(true); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID(boolean isSecure) + { + final Random ng = isSecure ? Holder.numberGenerator : getRandom(); + + byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(randomBytes); + } + + /** + * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 + * + * @param name 用于构造 UUID 的字节数组。 + * + * @return 根据指定数组生成的 {@code UUID} + */ + public static UUID nameUUIDFromBytes(byte[] name) + { + MessageDigest md; + try + { + md = MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException nsae) + { + throw new InternalError("MD5 not supported"); + } + byte[] md5Bytes = md.digest(name); + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(md5Bytes); + } + + /** + * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 + * + * @param name 指定 {@code UUID} 字符串 + * @return 具有指定值的 {@code UUID} + * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 + * + */ + public static UUID fromString(String name) + { + String[] components = name.split("-"); + if (components.length != 5) + { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + for (int i = 0; i < 5; i++) + { + components[i] = "0x" + components[i]; + } + + long mostSigBits = Long.decode(components[0]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]).longValue(); + + long leastSigBits = Long.decode(components[3]).longValue(); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]).longValue(); + + return new UUID(mostSigBits, leastSigBits); + } + + /** + * 返回此 UUID 的 128 位值中的最低有效 64 位。 + * + * @return 此 UUID 的 128 位值中的最低有效 64 位。 + */ + public long getLeastSignificantBits() + { + return leastSigBits; + } + + /** + * 返回此 UUID 的 128 位值中的最高有效 64 位。 + * + * @return 此 UUID 的 128 位值中最高有效 64 位。 + */ + public long getMostSignificantBits() + { + return mostSigBits; + } + + /** + * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 + *

+ * 版本号具有以下含意: + *

    + *
  • 1 基于时间的 UUID + *
  • 2 DCE 安全 UUID + *
  • 3 基于名称的 UUID + *
  • 4 随机生成的 UUID + *
+ * + * @return 此 {@code UUID} 的版本号 + */ + public int version() + { + // Version is bits masked by 0x000000000000F000 in MS long + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /** + * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 + *

+ * 变体号具有以下含意: + *

    + *
  • 0 为 NCS 向后兼容保留 + *
  • 2 IETF RFC 4122(Leach-Salz), 用于此类 + *
  • 6 保留,微软向后兼容 + *
  • 7 保留供以后定义使用 + *
+ * + * @return 此 {@code UUID} 相关联的变体号 + */ + public int variant() + { + // This field is composed of a varying number of bits. + // 0 - - Reserved for NCS backward compatibility + // 1 0 - The IETF aka Leach-Salz variant (used by this class) + // 1 1 0 Reserved, Microsoft backward compatibility + // 1 1 1 Reserved for future definition. + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + /** + * 与此 UUID 相关联的时间戳值。 + * + *

+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 + * + *

+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 + */ + public long timestamp() throws UnsupportedOperationException + { + checkTimeBase(); + return (mostSigBits & 0x0FFFL) << 48// + | ((mostSigBits >> 16) & 0x0FFFFL) << 32// + | mostSigBits >>> 32; + } + + /** + * 与此 UUID 相关联的时钟序列值。 + * + *

+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 + *

+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 + * UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的时钟序列 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public int clockSequence() throws UnsupportedOperationException + { + checkTimeBase(); + return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + } + + /** + * 与此 UUID 相关的节点值。 + * + *

+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 + *

+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的节点值 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public long node() throws UnsupportedOperationException + { + checkTimeBase(); + return leastSigBits & 0x0000FFFFFFFFFFFFL; + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @return 此{@code UUID} 的字符串表现形式 + * @see #toString(boolean) + */ + @Override + public String toString() + { + return toString(false); + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 + * @return 此{@code UUID} 的字符串表现形式 + */ + public String toString(boolean isSimple) + { + final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36); + // time_low + builder.append(digits(mostSigBits >> 32, 8)); + if (!isSimple) + { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (!isSimple) + { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (!isSimple) + { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (!isSimple) + { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + @Override + public int hashCode() + { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + /** + * 将此对象与指定对象比较。 + *

+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 + * + * @param obj 要与之比较的对象 + * + * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) + { + if ((null == obj) || (obj.getClass() != UUID.class)) + { + return false; + } + UUID id = (UUID) obj; + return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); + } + + // Comparison Operations + + /** + * 将此 UUID 与指定的 UUID 比较。 + * + *

+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 + * + * @param val 与此 UUID 比较的 UUID + * + * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 + * + */ + @Override + public int compareTo(UUID val) + { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + return (this.mostSigBits < val.mostSigBits ? -1 : // + (this.mostSigBits > val.mostSigBits ? 1 : // + (this.leastSigBits < val.leastSigBits ? -1 : // + (this.leastSigBits > val.leastSigBits ? 1 : // + 0)))); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Private method start + /** + * 返回指定数字对应的hex值 + * + * @param val 值 + * @param digits 位 + * @return 值 + */ + private static String digits(long val, int digits) + { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * 检查是否为time-based版本UUID + */ + private void checkTimeBase() + { + if (version() != 1) + { + throw new UnsupportedOperationException("Not a time-based UUID"); + } + } + + /** + * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) + * + * @return {@link SecureRandom} + */ + public static SecureRandom getSecureRandom() + { + try + { + return SecureRandom.getInstance("SHA1PRNG"); + } + catch (NoSuchAlgorithmException e) + { + throw new UtilException(e); + } + } + + /** + * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + * @return {@link ThreadLocalRandom} + */ + public static ThreadLocalRandom getRandom() + { + return ThreadLocalRandom.current(); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/validate/Checker.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/validate/Checker.java new file mode 100644 index 0000000..8871db3 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/validate/Checker.java @@ -0,0 +1,18 @@ +package com.storm.common.core.validate; + +/** + * 实现后在接口访问时如果接口实现了这个接口 + * 会被自动自行接口check进行校验 + **/ +public interface Checker { + + /** + * 用于实现validation不能校验的数据逻辑 + */ + default void check(){ + + } + + default void check(T data){ + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/controller/BaseController.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/controller/BaseController.java new file mode 100644 index 0000000..2c5dfba --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/controller/BaseController.java @@ -0,0 +1,160 @@ +package com.storm.common.core.web.controller; + +import java.beans.PropertyEditorSupport; +import java.util.Date; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.storm.common.core.constant.HttpStatus; +import com.storm.common.core.utils.DateUtils; +import com.storm.common.core.utils.PageUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.sql.SqlUtil; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.PageDomain; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.core.web.page.TableSupport; + +/** + * web层通用数据处理 + * + * @author ruoyi + */ +public class BaseController +{ + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * 将前台传递过来的日期格式的字符串,自动转化为Date类型 + */ + @InitBinder + public void initBinder(WebDataBinder binder) + { + // Date 类型转换 + binder.registerCustomEditor(Date.class, new PropertyEditorSupport() + { + @Override + public void setAsText(String text) + { + setValue(DateUtils.parseDate(text)); + } + }); + } + + /** + * 设置请求分页数据 + */ + protected void startPage() + { + PageUtils.startPage(); + } + + /** + * 设置请求排序数据 + */ + protected void startOrderBy() + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) + { + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + PageHelper.orderBy(orderBy); + } + } + + /** + * 清理分页的线程变量 + */ + protected void clearPage() + { + PageUtils.clearPage(); + } + + /** + * 响应请求分页数据 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected TableDataInfo getDataTable(List list) + { + TableDataInfo rspData = new TableDataInfo(); + rspData.setCode(HttpStatus.SUCCESS); + rspData.setRows(list); + rspData.setMsg("查询成功"); + rspData.setTotal(new PageInfo(list).getTotal()); + return rspData; + } + + /** + * 返回成功 + */ + public AjaxResult success() + { + return AjaxResult.success(); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(String message) + { + return AjaxResult.success(message); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(Object data) + { + return AjaxResult.success(data); + } + + /** + * 返回失败消息 + */ + public AjaxResult error() + { + return AjaxResult.error(); + } + + /** + * 返回失败消息 + */ + public AjaxResult error(String message) + { + return AjaxResult.error(message); + } + + /** + * 返回警告消息 + */ + public AjaxResult warn(String message) + { + return AjaxResult.warn(message); + } + + /** + * 响应返回结果 + * + * @param rows 影响行数 + * @return 操作结果 + */ + protected AjaxResult toAjax(int rows) + { + return rows > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 响应返回结果 + * + * @param result 结果 + * @return 操作结果 + */ + protected AjaxResult toAjax(boolean result) + { + return result ? success() : error(); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/domain/AjaxResult.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/domain/AjaxResult.java new file mode 100644 index 0000000..0686dc5 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/domain/AjaxResult.java @@ -0,0 +1,216 @@ +package com.storm.common.core.web.domain; + +import java.util.HashMap; +import java.util.Objects; +import com.storm.common.core.constant.HttpStatus; +import com.storm.common.core.utils.StringUtils; + +/** + * 操作消息提醒 + * + * @author ruoyi + */ +public class AjaxResult extends HashMap +{ + private static final long serialVersionUID = 1L; + + /** 状态码 */ + public static final String CODE_TAG = "code"; + + /** 返回内容 */ + public static final String MSG_TAG = "msg"; + + /** 数据对象 */ + public static final String DATA_TAG = "data"; + + /** + * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 + */ + public AjaxResult() + { + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + */ + public AjaxResult(int code, String msg) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + * @param data 数据对象 + */ + public AjaxResult(int code, String msg, Object data) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + if (StringUtils.isNotNull(data)) + { + super.put(DATA_TAG, data); + } + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static AjaxResult success() + { + return AjaxResult.success("操作成功"); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(Object data) + { + return AjaxResult.success("操作成功", data); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @return 成功消息 + */ + public static AjaxResult success(String msg) + { + return AjaxResult.success(msg, null); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 成功消息 + */ + public static AjaxResult success(String msg, Object data) + { + return new AjaxResult(HttpStatus.SUCCESS, msg, data); + } + + /** + * 返回警告消息 + * + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult warn(String msg) + { + return AjaxResult.warn(msg, null); + } + + /** + * 返回警告消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 警告消息 + */ + public static AjaxResult warn(String msg, Object data) + { + return new AjaxResult(HttpStatus.WARN, msg, data); + } + + /** + * 返回错误消息 + * + * @return 错误消息 + */ + public static AjaxResult error() + { + return AjaxResult.error("操作失败"); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(String msg) + { + return AjaxResult.error(msg, null); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 错误消息 + */ + public static AjaxResult error(String msg, Object data) + { + return new AjaxResult(HttpStatus.ERROR, msg, data); + } + + /** + * 返回错误消息 + * + * @param code 状态码 + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(int code, String msg) + { + return new AjaxResult(code, msg, null); + } + + /** + * 是否为成功消息 + * + * @return 结果 + */ + public boolean isSuccess() + { + return Objects.equals(HttpStatus.SUCCESS, this.get(CODE_TAG)); + } + + /** + * 是否为警告消息 + * + * @return 结果 + */ + public boolean isWarn() + { + return Objects.equals(HttpStatus.WARN, this.get(CODE_TAG)); + } + + /** + * 是否为错误消息 + * + * @return 结果 + */ + public boolean isError() + { + return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG)); + } + + /** + * 方便链式调用 + * + * @param key + * @param value + * @return + */ + @Override + public AjaxResult put(String key, Object value) + { + super.put(key, value); + return this; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/domain/BaseEntity.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/domain/BaseEntity.java new file mode 100644 index 0000000..7ced61a --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/domain/BaseEntity.java @@ -0,0 +1,127 @@ +package com.storm.common.core.web.domain; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Entity基类 + * + * @author ruoyi + */ +public class BaseEntity implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 搜索值 */ + @JsonIgnore + @TableField(exist = false) + private String searchValue; + + /** 创建者 */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @TableField(fill = FieldFill.INSERT) + private Date createTime; + + /** 更新者 */ + @TableField(fill = FieldFill.UPDATE) + private String updateBy; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @TableField(fill = FieldFill.UPDATE) + private Date updateTime; + + /** 备注 */ + private String remark; + + /** 请求参数 */ + @TableField(exist = false) + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map params; + + public String getSearchValue() + { + return searchValue; + } + + public void setSearchValue(String searchValue) + { + this.searchValue = searchValue; + } + + public String getCreateBy() + { + return createBy; + } + + public void setCreateBy(String createBy) + { + this.createBy = createBy; + } + + public Date getCreateTime() + { + return createTime; + } + + public void setCreateTime(Date createTime) + { + this.createTime = createTime; + } + + public String getUpdateBy() + { + return updateBy; + } + + public void setUpdateBy(String updateBy) + { + this.updateBy = updateBy; + } + + public Date getUpdateTime() + { + return updateTime; + } + + public void setUpdateTime(Date updateTime) + { + this.updateTime = updateTime; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } + + public Map getParams() + { + if (params == null) + { + params = new HashMap<>(); + } + return params; + } + + public void setParams(Map params) + { + this.params = params; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/domain/TreeEntity.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/domain/TreeEntity.java new file mode 100644 index 0000000..5ccbbc8 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/domain/TreeEntity.java @@ -0,0 +1,79 @@ +package com.storm.common.core.web.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tree基类 + * + * @author ruoyi + */ +public class TreeEntity extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 祖级列表 */ + private String ancestors; + + /** 子部门 */ + private List children = new ArrayList<>(); + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + public String getAncestors() + { + return ancestors; + } + + public void setAncestors(String ancestors) + { + this.ancestors = ancestors; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/page/PageDomain.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/page/PageDomain.java new file mode 100644 index 0000000..9a18f8f --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/page/PageDomain.java @@ -0,0 +1,101 @@ +package com.storm.common.core.web.page; + +import com.storm.common.core.utils.StringUtils; + +/** + * 分页数据 + * + * @author ruoyi + */ +public class PageDomain +{ + /** 当前记录起始索引 */ + private Integer pageNum; + + /** 每页显示记录数 */ + private Integer pageSize; + + /** 排序列 */ + private String orderByColumn; + + /** 排序的方向desc或者asc */ + private String isAsc = "asc"; + + /** 分页参数合理化 */ + private Boolean reasonable = true; + + public String getOrderBy() + { + if (StringUtils.isEmpty(orderByColumn)) + { + return ""; + } + return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc; + } + + public Integer getPageNum() + { + return pageNum; + } + + public void setPageNum(Integer pageNum) + { + this.pageNum = pageNum; + } + + public Integer getPageSize() + { + return pageSize; + } + + public void setPageSize(Integer pageSize) + { + this.pageSize = pageSize; + } + + public String getOrderByColumn() + { + return orderByColumn; + } + + public void setOrderByColumn(String orderByColumn) + { + this.orderByColumn = orderByColumn; + } + + public String getIsAsc() + { + return isAsc; + } + + public void setIsAsc(String isAsc) + { + if (StringUtils.isNotEmpty(isAsc)) + { + // 兼容前端排序类型 + if ("ascending".equals(isAsc)) + { + isAsc = "asc"; + } + else if ("descending".equals(isAsc)) + { + isAsc = "desc"; + } + this.isAsc = isAsc; + } + } + + public Boolean getReasonable() + { + if (StringUtils.isNull(reasonable)) + { + return Boolean.TRUE; + } + return reasonable; + } + + public void setReasonable(Boolean reasonable) + { + this.reasonable = reasonable; + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/page/TableDataInfo.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/page/TableDataInfo.java new file mode 100644 index 0000000..4f3147c --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/page/TableDataInfo.java @@ -0,0 +1,85 @@ +package com.storm.common.core.web.page; + +import java.io.Serializable; +import java.util.List; + +/** + * 表格分页数据对象 + * + * @author ruoyi + */ +public class TableDataInfo implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 总记录数 */ + private long total; + + /** 列表数据 */ + private List rows; + + /** 消息状态码 */ + private int code; + + /** 消息内容 */ + private String msg; + + /** + * 表格数据对象 + */ + public TableDataInfo() + { + } + + /** + * 分页 + * + * @param list 列表数据 + * @param total 总记录数 + */ + public TableDataInfo(List list, long total) + { + this.rows = list; + this.total = total; + } + + public long getTotal() + { + return total; + } + + public void setTotal(long total) + { + this.total = total; + } + + public List getRows() + { + return rows; + } + + public void setRows(List rows) + { + this.rows = rows; + } + + public int getCode() + { + return code; + } + + public void setCode(int code) + { + this.code = code; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/page/TableSupport.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/page/TableSupport.java new file mode 100644 index 0000000..2bee3ac --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/web/page/TableSupport.java @@ -0,0 +1,56 @@ +package com.storm.common.core.web.page; + +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.ServletUtils; + +/** + * 表格数据处理 + * + * @author ruoyi + */ +public class TableSupport +{ + /** + * 当前记录起始索引 + */ + public static final String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + public static final String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + public static final String IS_ASC = "isAsc"; + + /** + * 分页参数合理化 + */ + public static final String REASONABLE = "reasonable"; + + /** + * 封装分页对象 + */ + public static PageDomain getPageDomain() + { + PageDomain pageDomain = new PageDomain(); + pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1)); + pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10)); + pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN)); + pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC)); + pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE)); + return pageDomain; + } + + public static PageDomain buildPageRequest() + { + return getPageDomain(); + } +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/xss/Xss.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/xss/Xss.java new file mode 100644 index 0000000..6651b88 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/xss/Xss.java @@ -0,0 +1,27 @@ +package com.storm.common.core.xss; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义xss校验注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) +@Constraint(validatedBy = { XssValidator.class }) +public @interface Xss +{ + String message() + + default "不允许任何脚本运行"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/storm-common/storm-common-core/src/main/java/com/storm/common/core/xss/XssValidator.java b/storm-common/storm-common-core/src/main/java/com/storm/common/core/xss/XssValidator.java new file mode 100644 index 0000000..7056f77 --- /dev/null +++ b/storm-common/storm-common-core/src/main/java/com/storm/common/core/xss/XssValidator.java @@ -0,0 +1,39 @@ +package com.storm.common.core.xss; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import com.storm.common.core.utils.StringUtils; + +/** + * 自定义xss校验注解实现 + * + * @author ruoyi + */ +public class XssValidator implements ConstraintValidator +{ + private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) + { + if (StringUtils.isBlank(value)) + { + return true; + } + return !containsHtml(value); + } + + public static boolean containsHtml(String value) + { + StringBuilder sHtml = new StringBuilder(); + Pattern pattern = Pattern.compile(HTML_PATTERN); + Matcher matcher = pattern.matcher(value); + while (matcher.find()) + { + sHtml.append(matcher.group()); + } + return pattern.matcher(sHtml).matches(); + } +} \ No newline at end of file diff --git a/storm-common/storm-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/storm-common/storm-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..69fbcd6 --- /dev/null +++ b/storm-common/storm-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.storm.common.core.utils.SpringUtils diff --git a/storm-common/storm-common-datascope/pom.xml b/storm-common/storm-common-datascope/pom.xml new file mode 100644 index 0000000..e7e0833 --- /dev/null +++ b/storm-common/storm-common-datascope/pom.xml @@ -0,0 +1,27 @@ + + + + com.storm + storm-common + 3.6.6 + + 4.0.0 + + storm-common-datascope + + + storm-common-datascope权限范围 + + + + + + + com.storm + storm-common-security + + + + \ No newline at end of file diff --git a/storm-common/storm-common-datascope/src/main/java/com/storm/common/datascope/annotation/DataScope.java b/storm-common/storm-common-datascope/src/main/java/com/storm/common/datascope/annotation/DataScope.java new file mode 100644 index 0000000..70bd55d --- /dev/null +++ b/storm-common/storm-common-datascope/src/main/java/com/storm/common/datascope/annotation/DataScope.java @@ -0,0 +1,33 @@ +package com.storm.common.datascope.annotation; + +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; + +/** + * 数据权限过滤注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataScope +{ + /** + * 部门表的别名 + */ + public String deptAlias() default ""; + + /** + * 用户表的别名 + */ + public String userAlias() default ""; + + /** + * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@RequiresPermissions获取,多个权限用逗号分隔开来 + */ + public String permission() default ""; +} diff --git a/storm-common/storm-common-datascope/src/main/java/com/storm/common/datascope/aspect/DataScopeAspect.java b/storm-common/storm-common-datascope/src/main/java/com/storm/common/datascope/aspect/DataScopeAspect.java new file mode 100644 index 0000000..d6d9039 --- /dev/null +++ b/storm-common/storm-common-datascope/src/main/java/com/storm/common/datascope/aspect/DataScopeAspect.java @@ -0,0 +1,184 @@ +package com.storm.common.datascope.aspect; + +import java.util.ArrayList; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.context.SecurityContextHolder; +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.web.domain.BaseEntity; +import com.storm.common.datascope.annotation.DataScope; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysRole; +import com.storm.system.api.domain.SysUser; +import com.storm.system.api.model.LoginUser; + +/** + * 数据过滤处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class DataScopeAspect +{ + /** + * 全部数据权限 + */ + public static final String DATA_SCOPE_ALL = "1"; + + /** + * 自定数据权限 + */ + public static final String DATA_SCOPE_CUSTOM = "2"; + + /** + * 部门数据权限 + */ + public static final String DATA_SCOPE_DEPT = "3"; + + /** + * 部门及以下数据权限 + */ + public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; + + /** + * 仅本人数据权限 + */ + public static final String DATA_SCOPE_SELF = "5"; + + /** + * 数据权限过滤关键字 + */ + public static final String DATA_SCOPE = "dataScope"; + + @Before("@annotation(controllerDataScope)") + public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable + { + clearDataScope(point); + handleDataScope(point, controllerDataScope); + } + + protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) + { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNotNull(loginUser)) + { + SysUser currentUser = loginUser.getSysUser(); + // 如果是超级管理员,则不过滤数据 + if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) + { + String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), SecurityContextHolder.getPermission()); + dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), controllerDataScope.userAlias(), permission); + } + } + } + + /** + * 数据范围过滤 + * + * @param joinPoint 切点 + * @param user 用户 + * @param deptAlias 部门别名 + * @param userAlias 用户别名 + * @param permission 权限字符 + */ + public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission) + { + StringBuilder sqlString = new StringBuilder(); + List conditions = new ArrayList(); + List scopeCustomIds = new ArrayList(); + user.getRoles().forEach(role -> { + if (DATA_SCOPE_CUSTOM.equals(role.getDataScope()) && StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) + { + scopeCustomIds.add(Convert.toStr(role.getRoleId())); + } + }); + + for (SysRole role : user.getRoles()) + { + String dataScope = role.getDataScope(); + if (conditions.contains(dataScope) || StringUtils.equals(role.getStatus(), UserConstants.ROLE_DISABLE)) + { + continue; + } + if (!StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) + { + continue; + } + if (DATA_SCOPE_ALL.equals(dataScope)) + { + sqlString = new StringBuilder(); + conditions.add(dataScope); + break; + } + else if (DATA_SCOPE_CUSTOM.equals(dataScope)) + { + if (scopeCustomIds.size() > 1) + { + // 多个自定数据权限使用in查询,避免多次拼接。 + sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id in ({}) ) ", deptAlias, String.join(",", scopeCustomIds))); + } + else + { + sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, role.getRoleId())); + } + } + else if (DATA_SCOPE_DEPT.equals(dataScope)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId())); + } + else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", deptAlias, user.getDeptId(), user.getDeptId())); + } + else if (DATA_SCOPE_SELF.equals(dataScope)) + { + if (StringUtils.isNotBlank(userAlias)) + { + sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId())); + } + else + { + // 数据权限为仅本人且没有userAlias别名不查询任何数据 + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + } + conditions.add(dataScope); + } + + // 角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据 + if (StringUtils.isEmpty(conditions)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + + if (StringUtils.isNotBlank(sqlString.toString())) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")"); + } + } + } + + /** + * 拼接权限sql前先清空params.dataScope参数防止注入 + */ + private void clearDataScope(final JoinPoint joinPoint) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, ""); + } + } +} diff --git a/storm-common/storm-common-datascope/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/storm-common/storm-common-datascope/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..a775a24 --- /dev/null +++ b/storm-common/storm-common-datascope/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.storm.common.datascope.aspect.DataScopeAspect diff --git a/storm-common/storm-common-datasource/pom.xml b/storm-common/storm-common-datasource/pom.xml new file mode 100644 index 0000000..92fcb92 --- /dev/null +++ b/storm-common/storm-common-datasource/pom.xml @@ -0,0 +1,35 @@ + + + + com.storm + storm-common + 3.6.6 + + 4.0.0 + + storm-common-datasource + + + storm-common-datasource多数据源 + + + + + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + + + com.baomidou + dynamic-datasource-spring-boot-starter + ${dynamic-ds.version} + + + + \ No newline at end of file diff --git a/storm-common/storm-common-datasource/src/main/java/com/storm/common/datasource/annotation/Master.java b/storm-common/storm-common-datasource/src/main/java/com/storm/common/datasource/annotation/Master.java new file mode 100644 index 0000000..4d32578 --- /dev/null +++ b/storm-common/storm-common-datasource/src/main/java/com/storm/common/datasource/annotation/Master.java @@ -0,0 +1,22 @@ +package com.storm.common.datasource.annotation; + +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; +import com.baomidou.dynamic.datasource.annotation.DS; + +/** + * 主库数据源 + * + * @author ruoyi + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@DS("master") +public @interface Master +{ + +} \ No newline at end of file diff --git a/storm-common/storm-common-datasource/src/main/java/com/storm/common/datasource/annotation/Slave.java b/storm-common/storm-common-datasource/src/main/java/com/storm/common/datasource/annotation/Slave.java new file mode 100644 index 0000000..32579bf --- /dev/null +++ b/storm-common/storm-common-datasource/src/main/java/com/storm/common/datasource/annotation/Slave.java @@ -0,0 +1,22 @@ +package com.storm.common.datasource.annotation; + +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; +import com.baomidou.dynamic.datasource.annotation.DS; + +/** + * 从库数据源 + * + * @author ruoyi + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@DS("slave") +public @interface Slave +{ + +} \ No newline at end of file diff --git a/storm-common/storm-common-log/pom.xml b/storm-common/storm-common-log/pom.xml new file mode 100644 index 0000000..12719b3 --- /dev/null +++ b/storm-common/storm-common-log/pom.xml @@ -0,0 +1,27 @@ + + + + com.storm + storm-common + 3.6.6 + + 4.0.0 + + storm-common-log + + + storm-common-log日志记录 + + + + + + + com.storm + storm-common-security + + + + \ No newline at end of file diff --git a/storm-common/storm-common-log/src/main/java/com/storm/common/log/annotation/Log.java b/storm-common/storm-common-log/src/main/java/com/storm/common/log/annotation/Log.java new file mode 100644 index 0000000..41fb0e2 --- /dev/null +++ b/storm-common/storm-common-log/src/main/java/com/storm/common/log/annotation/Log.java @@ -0,0 +1,51 @@ +package com.storm.common.log.annotation; + +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; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.log.enums.OperatorType; + +/** + * 自定义操作日志记录注解 + * + * @author ruoyi + * + */ +@Target({ ElementType.PARAMETER, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log +{ + /** + * 模块 + */ + public String title() default ""; + + /** + * 功能 + */ + public BusinessType businessType() default BusinessType.OTHER; + + /** + * 操作人类别 + */ + public OperatorType operatorType() default OperatorType.MANAGE; + + /** + * 是否保存请求的参数 + */ + public boolean isSaveRequestData() default true; + + /** + * 是否保存响应的参数 + */ + public boolean isSaveResponseData() default true; + + /** + * 排除指定的请求参数 + */ + public String[] excludeParamNames() default {}; +} diff --git a/storm-common/storm-common-log/src/main/java/com/storm/common/log/aspect/LogAspect.java b/storm-common/storm-common-log/src/main/java/com/storm/common/log/aspect/LogAspect.java new file mode 100644 index 0000000..e09ed60 --- /dev/null +++ b/storm-common/storm-common-log/src/main/java/com/storm/common/log/aspect/LogAspect.java @@ -0,0 +1,250 @@ +package com.storm.common.log.aspect; + +import java.util.Collection; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.NamedThreadLocal; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.fastjson2.JSON; +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.ExceptionUtil; +import com.storm.common.core.utils.ServletUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.ip.IpUtils; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessStatus; +import com.storm.common.log.filter.PropertyPreExcludeFilter; +import com.storm.common.log.service.AsyncLogService; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysOperLog; + +/** + * 操作日志记录处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class LogAspect +{ + private static final Logger log = LoggerFactory.getLogger(LogAspect.class); + + /** 排除敏感属性字段 */ + public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" }; + + /** 计算操作消耗时间 */ + private static final ThreadLocal TIME_THREADLOCAL = new NamedThreadLocal("Cost Time"); + + @Autowired + private AsyncLogService asyncLogService; + + /** + * 处理请求前执行 + */ + @Before(value = "@annotation(controllerLog)") + public void doBefore(JoinPoint joinPoint, Log controllerLog) + { + TIME_THREADLOCAL.set(System.currentTimeMillis()); + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) + { + handleLog(joinPoint, controllerLog, null, jsonResult); + } + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) + { + handleLog(joinPoint, controllerLog, e, null); + } + + protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) + { + try + { + // *========数据库日志=========*// + SysOperLog operLog = new SysOperLog(); + operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); + // 请求的地址 + String ip = IpUtils.getIpAddr(); + operLog.setOperIp(ip); + operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); + String username = SecurityUtils.getUsername(); + if (StringUtils.isNotBlank(username)) + { + operLog.setOperName(username); + } + + if (e != null) + { + operLog.setStatus(BusinessStatus.FAIL.ordinal()); + operLog.setErrorMsg(StringUtils.substring(Convert.toStr(e.getMessage(), ExceptionUtil.getExceptionMessage(e)), 0, 2000)); + } + // 设置方法名称 + String className = joinPoint.getTarget().getClass().getName(); + String methodName = joinPoint.getSignature().getName(); + operLog.setMethod(className + "." + methodName + "()"); + // 设置请求方式 + operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); + // 处理设置注解上的参数 + getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); + // 设置消耗时间 + operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get()); + // 保存数据库 + asyncLogService.saveSysLog(operLog); + } + catch (Exception exp) + { + // 记录本地异常日志 + log.error("异常信息:{}", exp.getMessage()); + exp.printStackTrace(); + } + finally + { + TIME_THREADLOCAL.remove(); + } + } + + /** + * 获取注解中对方法的描述信息 用于Controller层注解 + * + * @param log 日志 + * @param operLog 操作日志 + * @throws Exception + */ + public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception + { + // 设置action动作 + operLog.setBusinessType(log.businessType().ordinal()); + // 设置标题 + operLog.setTitle(log.title()); + // 设置操作人类别 + operLog.setOperatorType(log.operatorType().ordinal()); + // 是否需要保存request,参数和值 + if (log.isSaveRequestData()) + { + // 获取参数的信息,传入到数据库中。 + setRequestValue(joinPoint, operLog, log.excludeParamNames()); + } + // 是否需要保存response,参数和值 + if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) + { + operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); + } + } + + /** + * 获取请求的参数,放到log中 + * + * @param operLog 操作日志 + * @throws Exception 异常 + */ + private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception + { + Map paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest()); + String requestMethod = operLog.getRequestMethod(); + if (StringUtils.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) + { + String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames); + operLog.setOperParam(StringUtils.substring(params, 0, 2000)); + } + else + { + operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000)); + } + } + + /** + * 参数拼装 + */ + private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) + { + String params = ""; + if (paramsArray != null && paramsArray.length > 0) + { + for (Object o : paramsArray) + { + if (StringUtils.isNotNull(o) && !isFilterObject(o)) + { + try + { + String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames)); + params += jsonObj.toString() + " "; + } + catch (Exception e) + { + } + } + } + } + return params.trim(); + } + + /** + * 忽略敏感属性 + */ + public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames) + { + return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames)); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) + { + Class clazz = o.getClass(); + if (clazz.isArray()) + { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } + else if (Collection.class.isAssignableFrom(clazz)) + { + Collection collection = (Collection) o; + for (Object value : collection) + { + return value instanceof MultipartFile; + } + } + else if (Map.class.isAssignableFrom(clazz)) + { + Map map = (Map) o; + for (Object value : map.entrySet()) + { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse + || o instanceof BindingResult; + } +} diff --git a/storm-common/storm-common-log/src/main/java/com/storm/common/log/enums/BusinessStatus.java b/storm-common/storm-common-log/src/main/java/com/storm/common/log/enums/BusinessStatus.java new file mode 100644 index 0000000..633cc9b --- /dev/null +++ b/storm-common/storm-common-log/src/main/java/com/storm/common/log/enums/BusinessStatus.java @@ -0,0 +1,20 @@ +package com.storm.common.log.enums; + +/** + * 操作状态 + * + * @author ruoyi + * + */ +public enum BusinessStatus +{ + /** + * 成功 + */ + SUCCESS, + + /** + * 失败 + */ + FAIL, +} diff --git a/storm-common/storm-common-log/src/main/java/com/storm/common/log/enums/BusinessType.java b/storm-common/storm-common-log/src/main/java/com/storm/common/log/enums/BusinessType.java new file mode 100644 index 0000000..52f1568 --- /dev/null +++ b/storm-common/storm-common-log/src/main/java/com/storm/common/log/enums/BusinessType.java @@ -0,0 +1,59 @@ +package com.storm.common.log.enums; + +/** + * 业务操作类型 + * + * @author ruoyi + */ +public enum BusinessType +{ + /** + * 其它 + */ + OTHER, + + /** + * 新增 + */ + INSERT, + + /** + * 修改 + */ + UPDATE, + + /** + * 删除 + */ + DELETE, + + /** + * 授权 + */ + GRANT, + + /** + * 导出 + */ + EXPORT, + + /** + * 导入 + */ + IMPORT, + + /** + * 强退 + */ + FORCE, + + /** + * 生成代码 + */ + GENCODE, + + /** + * 清空数据 + */ + CLEAN, +} diff --git a/storm-common/storm-common-log/src/main/java/com/storm/common/log/enums/OperatorType.java b/storm-common/storm-common-log/src/main/java/com/storm/common/log/enums/OperatorType.java new file mode 100644 index 0000000..08dc603 --- /dev/null +++ b/storm-common/storm-common-log/src/main/java/com/storm/common/log/enums/OperatorType.java @@ -0,0 +1,24 @@ +package com.storm.common.log.enums; + +/** + * 操作人类别 + * + * @author ruoyi + */ +public enum OperatorType +{ + /** + * 其它 + */ + OTHER, + + /** + * 后台用户 + */ + MANAGE, + + /** + * 手机端用户 + */ + MOBILE +} diff --git a/storm-common/storm-common-log/src/main/java/com/storm/common/log/filter/PropertyPreExcludeFilter.java b/storm-common/storm-common-log/src/main/java/com/storm/common/log/filter/PropertyPreExcludeFilter.java new file mode 100644 index 0000000..a62acef --- /dev/null +++ b/storm-common/storm-common-log/src/main/java/com/storm/common/log/filter/PropertyPreExcludeFilter.java @@ -0,0 +1,24 @@ +package com.storm.common.log.filter; + +import com.alibaba.fastjson2.filter.SimplePropertyPreFilter; + +/** + * 排除JSON敏感属性 + * + * @author ruoyi + */ +public class PropertyPreExcludeFilter extends SimplePropertyPreFilter +{ + public PropertyPreExcludeFilter() + { + } + + public PropertyPreExcludeFilter addExcludes(String... filters) + { + for (int i = 0; i < filters.length; i++) + { + this.getExcludes().add(filters[i]); + } + return this; + } +} diff --git a/storm-common/storm-common-log/src/main/java/com/storm/common/log/service/AsyncLogService.java b/storm-common/storm-common-log/src/main/java/com/storm/common/log/service/AsyncLogService.java new file mode 100644 index 0000000..c1e3cc5 --- /dev/null +++ b/storm-common/storm-common-log/src/main/java/com/storm/common/log/service/AsyncLogService.java @@ -0,0 +1,29 @@ +package com.storm.common.log.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.system.api.RemoteLogService; +import com.storm.system.api.domain.SysOperLog; + +/** + * 异步调用日志服务 + * + * @author ruoyi + */ +@Service +public class AsyncLogService +{ + @Autowired + private RemoteLogService remoteLogService; + + /** + * 保存系统日志记录 + */ + @Async + public void saveSysLog(SysOperLog sysOperLog) throws Exception + { + remoteLogService.saveLog(sysOperLog, SecurityConstants.INNER); + } +} diff --git a/storm-common/storm-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/storm-common/storm-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..d67e77d --- /dev/null +++ b/storm-common/storm-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.storm.common.log.service.AsyncLogService +com.storm.common.log.aspect.LogAspect diff --git a/storm-common/storm-common-oss/pom.xml b/storm-common/storm-common-oss/pom.xml new file mode 100644 index 0000000..0161089 --- /dev/null +++ b/storm-common/storm-common-oss/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + com.storm + storm + 3.6.6 + ../../pom.xml + + + storm-common-oss + + + 11 + 11 + UTF-8 + + + + + com.storm + storm-common-core + + + + com.huaweicloud + esdk-obs-java + + + + org.projectlombok + lombok + + + + \ No newline at end of file diff --git a/storm-common/storm-common-oss/src/main/java/com/storm/oss/config/HuaweiObsAutoConfiguration.java b/storm-common/storm-common-oss/src/main/java/com/storm/oss/config/HuaweiObsAutoConfiguration.java new file mode 100644 index 0000000..7c2e566 --- /dev/null +++ b/storm-common/storm-common-oss/src/main/java/com/storm/oss/config/HuaweiObsAutoConfiguration.java @@ -0,0 +1,19 @@ +package com.storm.oss.config; + +import com.storm.oss.service.ObsHuaweiFileStorageService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +@EnableConfigurationProperties(HuaweiObsConfigProperties.class) +public class HuaweiObsAutoConfiguration { + + @Bean + public ObsHuaweiFileStorageService obsHuaweiFileStorageService(HuaweiObsConfigProperties properties) { + log.info("创建华为OBS存储服务,Bucket: {}", properties.getBucketName()); + return new ObsHuaweiFileStorageService(properties); + } +} \ No newline at end of file diff --git a/storm-common/storm-common-oss/src/main/java/com/storm/oss/config/HuaweiObsConfigProperties.java b/storm-common/storm-common-oss/src/main/java/com/storm/oss/config/HuaweiObsConfigProperties.java new file mode 100644 index 0000000..1904008 --- /dev/null +++ b/storm-common/storm-common-oss/src/main/java/com/storm/oss/config/HuaweiObsConfigProperties.java @@ -0,0 +1,41 @@ +package com.storm.oss.config; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + + +@Setter +@Getter +@NoArgsConstructor +@ToString +@Configuration +@ConfigurationProperties(prefix = "storm.huaweiobs") +public class HuaweiObsConfigProperties { + + /** + * 域名站点 + */ + private String ENDPOINT ; + + /** + * 秘钥Id + */ + private String CLOUD_SDK_AK ; + + /** + * 秘钥 + */ + private String CLOUD_SDK_SK ; + + /** + * 桶名称 + */ + private String bucketName ; + + +} + diff --git a/storm-common/storm-common-oss/src/main/java/com/storm/oss/service/ObsHuaweiFileStorageService.java b/storm-common/storm-common-oss/src/main/java/com/storm/oss/service/ObsHuaweiFileStorageService.java new file mode 100644 index 0000000..31ee8e2 --- /dev/null +++ b/storm-common/storm-common-oss/src/main/java/com/storm/oss/service/ObsHuaweiFileStorageService.java @@ -0,0 +1,108 @@ +package com.storm.oss.service; + + +import com.obs.services.ObsClient; +import com.obs.services.exception.ObsException; +import com.obs.services.model.PutObjectRequest; +import com.obs.services.model.PutObjectResult; +import com.storm.oss.config.HuaweiObsConfigProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.InputStream; + +@Slf4j +public class ObsHuaweiFileStorageService { + + private HuaweiObsConfigProperties huaweiObsConfigProperties; + + public ObsHuaweiFileStorageService(HuaweiObsConfigProperties huaweiObsConfigProperties) { + this.huaweiObsConfigProperties = huaweiObsConfigProperties; + log.info("华为OBS服务初始化成功,Bucket: {}", huaweiObsConfigProperties.getBucketName()); + } + + /** + * 上传文件 + * @param objectName 文件名 + * @param inputStream 文件流对象 + * @return + */ + public String store(String objectName, InputStream inputStream) { + //文件读取路径 + String url = null; + + // 判断文件 + if (inputStream == null) { + log.error("上传文件:objectName{}文件流为空", objectName); + return url; + } + // 创建ObsClient实例 + // 使用永久AK/SK初始化客户端 + ObsClient obsClient = new ObsClient(huaweiObsConfigProperties.getCLOUD_SDK_AK(), huaweiObsConfigProperties.getCLOUD_SDK_SK() + ,huaweiObsConfigProperties.getENDPOINT()); + log.info("OSS文件上传开始:{}", objectName); + try { + // 上传文件 + PutObjectRequest request = new PutObjectRequest(huaweiObsConfigProperties.getBucketName(), objectName, inputStream); + + // 设置权限(公开读) + //obsClient.setBucketAcl(huaweiObsConfigProperties.getBucketName(),AccessControlList.REST_CANNED_PUBLIC_READ_WRITE); + + + PutObjectResult result = obsClient.putObject(request); + url = result.getObjectUrl(); + + if (result != null) { + log.info("OSS文件上传成功:{}", objectName); + + } + } catch (ObsException e) { + log.error("OSS文件上传错误:{}", e); + System.out.println("putObject failed"); + // 请求失败,打印http状态码 + System.out.println("HTTP Code:" + e.getResponseCode()); + // 请求失败,打印服务端错误码 + System.out.println("Error Code:" + e.getErrorCode()); + // 请求失败,打印详细错误信息 + System.out.println("Error Message:" + e.getErrorMessage()); + // 请求失败,打印请求id + System.out.println("Request ID:" + e.getErrorRequestId()); + System.out.println("Host ID:" + e.getErrorHostId()); + e.printStackTrace(); + } catch (Exception e) { + log.error("OSS文件上传客户端错误:{}", e); + System.out.println("putObject failed"); + // 其他异常信息打印 + e.printStackTrace(); + } +// //文件访问路径规则 https://BucketName.Endpoint/ObjectName +// StringBuilder stringBuilder = new StringBuilder("https://"); +// stringBuilder +// .append(huaweiObsConfigProperties.getBucketName()) +// .append(".") +// .append(huaweiObsConfigProperties.getENDPOINT()) +// .append("/") +// .append(objectName); + return url; + + } + + /** + * 根据url删除文件 + * @param pathUrl url地址(全路径) + */ + public void delete(String pathUrl) { + + ObsClient obsClient = new ObsClient(huaweiObsConfigProperties.getCLOUD_SDK_AK(), huaweiObsConfigProperties.getCLOUD_SDK_SK() + ,huaweiObsConfigProperties.getENDPOINT()); + + String prefix = "https://"+huaweiObsConfigProperties.getBucketName()+"."+ huaweiObsConfigProperties.getENDPOINT()+"/"; + String key = pathUrl.replace(prefix, ""); + + // 删除单个对象 + obsClient.deleteObject(huaweiObsConfigProperties.getBucketName(), key); + + } + +} diff --git a/storm-common/storm-common-oss/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/storm-common/storm-common-oss/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..9aa5bc9 --- /dev/null +++ b/storm-common/storm-common-oss/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.storm.oss.config.HuaweiObsAutoConfiguration \ No newline at end of file diff --git a/storm-common/storm-common-redis/pom.xml b/storm-common/storm-common-redis/pom.xml new file mode 100644 index 0000000..64aaec7 --- /dev/null +++ b/storm-common/storm-common-redis/pom.xml @@ -0,0 +1,33 @@ + + + + com.storm + storm-common + 3.6.6 + + 4.0.0 + + storm-common-redis + + + storm-common-redis缓存服务 + + + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + com.storm + storm-common-core + + + + \ No newline at end of file diff --git a/storm-common/storm-common-redis/src/main/java/com/storm/common/redis/configure/FastJson2JsonRedisSerializer.java b/storm-common/storm-common-redis/src/main/java/com/storm/common/redis/configure/FastJson2JsonRedisSerializer.java new file mode 100644 index 0000000..aef7a01 --- /dev/null +++ b/storm-common/storm-common-redis/src/main/java/com/storm/common/redis/configure/FastJson2JsonRedisSerializer.java @@ -0,0 +1,52 @@ +package com.storm.common.redis.configure; + +import java.nio.charset.Charset; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.Filter; +import com.storm.common.core.constant.Constants; + +/** + * Redis使用FastJson序列化 + * + * @author ruoyi + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer +{ + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR); + + private Class clazz; + + public FastJson2JsonRedisSerializer(Class clazz) + { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException + { + if (t == null) + { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException + { + if (bytes == null || bytes.length <= 0) + { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + + return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER); + } +} diff --git a/storm-common/storm-common-redis/src/main/java/com/storm/common/redis/configure/RedisConfig.java b/storm-common/storm-common-redis/src/main/java/com/storm/common/redis/configure/RedisConfig.java new file mode 100644 index 0000000..36643ad --- /dev/null +++ b/storm-common/storm-common-redis/src/main/java/com/storm/common/redis/configure/RedisConfig.java @@ -0,0 +1,43 @@ +package com.storm.common.redis.configure; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * redis配置 + * + * @author ruoyi + */ +@Configuration +@EnableCaching +@AutoConfigureBefore(RedisAutoConfiguration.class) +public class RedisConfig extends CachingConfigurerSupport +{ + @Bean + @SuppressWarnings(value = { "unchecked", "rawtypes" }) + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } +} diff --git a/storm-common/storm-common-redis/src/main/java/com/storm/common/redis/service/RedisService.java b/storm-common/storm-common-redis/src/main/java/com/storm/common/redis/service/RedisService.java new file mode 100644 index 0000000..5db4d92 --- /dev/null +++ b/storm-common/storm-common-redis/src/main/java/com/storm/common/redis/service/RedisService.java @@ -0,0 +1,268 @@ +package com.storm.common.redis.service; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.BoundSetOperations; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +/** + * spring redis 工具类 + * + * @author ruoyi + **/ +@SuppressWarnings(value = { "unchecked", "rawtypes" }) +@Component +public class RedisService +{ + @Autowired + public RedisTemplate redisTemplate; + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) + { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit) + { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) + { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) + { + return redisTemplate.expire(key, timeout, unit); + } + + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) + { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) + { + return redisTemplate.hasKey(key); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) + { + ValueOperations operation = redisTemplate.opsForValue(); + return operation.get(key); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) + { + return redisTemplate.delete(key); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public boolean deleteObject(final Collection collection) + { + return redisTemplate.delete(collection) > 0; + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long setCacheList(final String key, final List dataList) + { + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) + { + return redisTemplate.opsForList().range(key, 0, -1); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public BoundSetOperations setCacheSet(final String key, final Set dataSet) + { + BoundSetOperations setOperation = redisTemplate.boundSetOps(key); + Iterator it = dataSet.iterator(); + while (it.hasNext()) + { + setOperation.add(it.next()); + } + return setOperation; + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) + { + return redisTemplate.opsForSet().members(key); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) + { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) + { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) + { + redisTemplate.opsForHash().put(key, hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) + { + HashOperations opsForHash = redisTemplate.opsForHash(); + return opsForHash.get(key, hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) + { + return redisTemplate.opsForHash().multiGet(key, hKeys); + } + + /** + * 删除Hash中的某条数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return 是否成功 + */ + public boolean deleteCacheMapValue(final String key, final String hKey) + { + return redisTemplate.opsForHash().delete(key, hKey) > 0; + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) + { + return redisTemplate.keys(pattern); + } +} diff --git a/storm-common/storm-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/storm-common/storm-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..42e3dae --- /dev/null +++ b/storm-common/storm-common-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.storm.common.redis.configure.RedisConfig +com.storm.common.redis.service.RedisService diff --git a/storm-common/storm-common-security/pom.xml b/storm-common/storm-common-security/pom.xml new file mode 100644 index 0000000..cd34835 --- /dev/null +++ b/storm-common/storm-common-security/pom.xml @@ -0,0 +1,40 @@ + + + + com.storm + storm-common + 3.6.6 + + 4.0.0 + + storm-common-security + + + storm-common-security安全模块 + + + + + + + + org.springframework + spring-webmvc + + + + + com.storm + storm-api-system + + + + + com.storm + storm-common-redis + + + + + diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/EnableCustomConfig.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/EnableCustomConfig.java new file mode 100644 index 0000000..37bee1d --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/EnableCustomConfig.java @@ -0,0 +1,31 @@ +package com.storm.common.security.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Import; +import org.springframework.scheduling.annotation.EnableAsync; +import com.storm.common.security.config.ApplicationConfig; +import com.storm.common.security.feign.FeignAutoConfiguration; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +// 表示通过aop框架暴露该代理对象,AopContext能够访问 +@EnableAspectJAutoProxy(exposeProxy = true) +// 指定要扫描的Mapper类的包的路径 +@MapperScan("com.storm.**.mapper") +// 开启线程异步执行 +@EnableAsync +// 自动加载类 +@Import({ ApplicationConfig.class, FeignAutoConfiguration.class }) +public @interface EnableCustomConfig +{ + +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/EnableRyFeignClients.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/EnableRyFeignClients.java new file mode 100644 index 0000000..265d640 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/EnableRyFeignClients.java @@ -0,0 +1,27 @@ +package com.storm.common.security.annotation; + +import org.springframework.cloud.openfeign.EnableFeignClients; +import java.lang.annotation.*; + +/** + * 自定义feign注解 + * 添加basePackages路径 + * + * @author ruoyi + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnableFeignClients +public @interface EnableRyFeignClients +{ + String[] value() default {}; + + String[] basePackages() default { "com.storm" }; + + Class[] basePackageClasses() default {}; + + Class[] defaultConfiguration() default {}; + + Class[] clients() default {}; +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/InnerAuth.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/InnerAuth.java new file mode 100644 index 0000000..25c1ba9 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/InnerAuth.java @@ -0,0 +1,19 @@ +package com.storm.common.security.annotation; + +import java.lang.annotation.*; + +/** + * 内部认证注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface InnerAuth +{ + /** + * 是否校验用户信息 + */ + boolean isUser() default false; +} \ No newline at end of file diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/Logical.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/Logical.java new file mode 100644 index 0000000..061f905 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/Logical.java @@ -0,0 +1,20 @@ +package com.storm.common.security.annotation; + +/** + * 权限注解的验证模式 + * + * @author ruoyi + * + */ +public enum Logical +{ + /** + * 必须具有所有的元素 + */ + AND, + + /** + * 只需具有其中一个元素 + */ + OR +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/RequiresLogin.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/RequiresLogin.java new file mode 100644 index 0000000..4739a21 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/RequiresLogin.java @@ -0,0 +1,18 @@ +package com.storm.common.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 登录认证:只有登录之后才能进入该方法 + * + * @author ruoyi + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RequiresLogin +{ +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/RequiresPermissions.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/RequiresPermissions.java new file mode 100644 index 0000000..4b01da9 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/RequiresPermissions.java @@ -0,0 +1,27 @@ +package com.storm.common.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 权限认证:必须具有指定权限才能进入该方法 + * + * @author ruoyi + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RequiresPermissions +{ + /** + * 需要校验的权限码 + */ + String[] value() default {}; + + /** + * 验证模式:AND | OR,默认AND + */ + Logical logical() default Logical.AND; +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/RequiresRoles.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/RequiresRoles.java new file mode 100644 index 0000000..f997a42 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/annotation/RequiresRoles.java @@ -0,0 +1,26 @@ +package com.storm.common.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 角色认证:必须具有指定角色标识才能进入该方法 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RequiresRoles +{ + /** + * 需要校验的角色标识 + */ + String[] value() default {}; + + /** + * 验证逻辑:AND | OR,默认AND + */ + Logical logical() default Logical.AND; +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/aspect/InnerAuthAspect.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/aspect/InnerAuthAspect.java new file mode 100644 index 0000000..0a9cc24 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/aspect/InnerAuthAspect.java @@ -0,0 +1,51 @@ +package com.storm.common.security.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.exception.InnerAuthException; +import com.storm.common.core.utils.ServletUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.security.annotation.InnerAuth; + +/** + * 内部服务调用验证处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class InnerAuthAspect implements Ordered +{ + @Around("@annotation(innerAuth)") + public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable + { + String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE); + // 内部请求验证 + if (!StringUtils.equals(SecurityConstants.INNER, source)) + { + throw new InnerAuthException("没有内部访问权限,不允许访问"); + } + + String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID); + String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME); + // 用户信息验证 + if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))) + { + throw new InnerAuthException("没有设置用户信息,不允许访问 "); + } + return point.proceed(); + } + + /** + * 确保在权限认证aop执行前执行 + */ + @Override + public int getOrder() + { + return Ordered.HIGHEST_PRECEDENCE + 1; + } +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/aspect/PreAuthorizeAspect.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/aspect/PreAuthorizeAspect.java new file mode 100644 index 0000000..a8ee4d0 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/aspect/PreAuthorizeAspect.java @@ -0,0 +1,89 @@ +package com.storm.common.security.aspect; + +import java.lang.reflect.Method; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import com.storm.common.security.annotation.RequiresLogin; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.annotation.RequiresRoles; +import com.storm.common.security.auth.AuthUtil; + +/** + * 基于 Spring Aop 的注解鉴权 + * + * @author kong + */ +@Aspect +@Component +public class PreAuthorizeAspect +{ + /** + * 构建 + */ + public PreAuthorizeAspect() + { + } + + /** + * 定义AOP签名 (切入所有使用鉴权注解的方法) + */ + public static final String POINTCUT_SIGN = " @annotation(com.storm.common.security.annotation.RequiresLogin) || " + + "@annotation(com.storm.common.security.annotation.RequiresPermissions) || " + + "@annotation(com.storm.common.security.annotation.RequiresRoles)"; + + /** + * 声明AOP签名 + */ + @Pointcut(POINTCUT_SIGN) + public void pointcut() + { + } + + /** + * 环绕切入 + * + * @param joinPoint 切面对象 + * @return 底层方法执行后的返回值 + * @throws Throwable 底层方法抛出的异常 + */ + @Around("pointcut()") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable + { + // 注解鉴权 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + checkMethodAnnotation(signature.getMethod()); + // 执行原有逻辑 + return joinPoint.proceed(); + } + + /** + * 对一个Method对象进行注解检查 + */ + public void checkMethodAnnotation(Method method) + { + // 校验 @RequiresLogin 注解 + RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class); + if (requiresLogin != null) + { + AuthUtil.checkLogin(); + } + + // 校验 @RequiresRoles 注解 + RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class); + if (requiresRoles != null) + { + AuthUtil.checkRole(requiresRoles); + } + + // 校验 @RequiresPermissions 注解 + RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class); + if (requiresPermissions != null) + { + AuthUtil.checkPermi(requiresPermissions); + } + } +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/auth/AuthLogic.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/auth/AuthLogic.java new file mode 100644 index 0000000..31a42ab --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/auth/AuthLogic.java @@ -0,0 +1,373 @@ +package com.storm.common.security.auth; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import org.springframework.util.PatternMatchUtils; +import com.storm.common.core.context.SecurityContextHolder; +import com.storm.common.core.exception.auth.NotLoginException; +import com.storm.common.core.exception.auth.NotPermissionException; +import com.storm.common.core.exception.auth.NotRoleException; +import com.storm.common.core.utils.SpringUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.security.annotation.Logical; +import com.storm.common.security.annotation.RequiresLogin; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.annotation.RequiresRoles; +import com.storm.common.security.service.TokenService; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.model.LoginUser; + +/** + * Token 权限验证,逻辑实现类 + * + * @author ruoyi + */ +public class AuthLogic +{ + /** 所有权限标识 */ + private static final String ALL_PERMISSION = "*:*:*"; + + /** 管理员角色权限标识 */ + private static final String SUPER_ADMIN = "admin"; + + public TokenService tokenService = SpringUtils.getBean(TokenService.class); + + /** + * 会话注销 + */ + public void logout() + { + String token = SecurityUtils.getToken(); + if (token == null) + { + return; + } + logoutByToken(token); + } + + /** + * 会话注销,根据指定Token + */ + public void logoutByToken(String token) + { + tokenService.delLoginUser(token); + } + + /** + * 检验用户是否已经登录,如未登录,则抛出异常 + */ + public void checkLogin() + { + getLoginUser(); + } + + /** + * 获取当前用户缓存信息, 如果未登录,则抛出异常 + * + * @return 用户缓存信息 + */ + public LoginUser getLoginUser() + { + String token = SecurityUtils.getToken(); + if (token == null) + { + throw new NotLoginException("未提供token"); + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser == null) + { + throw new NotLoginException("无效的token"); + } + return loginUser; + } + + /** + * 获取当前用户缓存信息, 如果未登录,则抛出异常 + * + * @param token 前端传递的认证信息 + * @return 用户缓存信息 + */ + public LoginUser getLoginUser(String token) + { + return tokenService.getLoginUser(token); + } + + /** + * 验证当前用户有效期, 如果相差不足120分钟,自动刷新缓存 + * + * @param loginUser 当前用户信息 + */ + public void verifyLoginUserExpire(LoginUser loginUser) + { + tokenService.verifyToken(loginUser); + } + + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public boolean hasPermi(String permission) + { + return hasPermi(getPermiList(), permission); + } + + /** + * 验证用户是否具备某权限, 如果验证未通过,则抛出异常: NotPermissionException + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public void checkPermi(String permission) + { + if (!hasPermi(getPermiList(), permission)) + { + throw new NotPermissionException(permission); + } + } + + /** + * 根据注解(@RequiresPermissions)鉴权, 如果验证未通过,则抛出异常: NotPermissionException + * + * @param requiresPermissions 注解对象 + */ + public void checkPermi(RequiresPermissions requiresPermissions) + { + SecurityContextHolder.setPermission(StringUtils.join(requiresPermissions.value(), ",")); + if (requiresPermissions.logical() == Logical.AND) + { + checkPermiAnd(requiresPermissions.value()); + } + else + { + checkPermiOr(requiresPermissions.value()); + } + } + + /** + * 验证用户是否含有指定权限,必须全部拥有 + * + * @param permissions 权限列表 + */ + public void checkPermiAnd(String... permissions) + { + Set permissionList = getPermiList(); + for (String permission : permissions) + { + if (!hasPermi(permissionList, permission)) + { + throw new NotPermissionException(permission); + } + } + } + + /** + * 验证用户是否含有指定权限,只需包含其中一个 + * + * @param permissions 权限码数组 + */ + public void checkPermiOr(String... permissions) + { + Set permissionList = getPermiList(); + for (String permission : permissions) + { + if (hasPermi(permissionList, permission)) + { + return; + } + } + if (permissions.length > 0) + { + throw new NotPermissionException(permissions); + } + } + + /** + * 判断用户是否拥有某个角色 + * + * @param role 角色标识 + * @return 用户是否具备某角色 + */ + public boolean hasRole(String role) + { + return hasRole(getRoleList(), role); + } + + /** + * 判断用户是否拥有某个角色, 如果验证未通过,则抛出异常: NotRoleException + * + * @param role 角色标识 + */ + public void checkRole(String role) + { + if (!hasRole(role)) + { + throw new NotRoleException(role); + } + } + + /** + * 根据注解(@RequiresRoles)鉴权 + * + * @param requiresRoles 注解对象 + */ + public void checkRole(RequiresRoles requiresRoles) + { + if (requiresRoles.logical() == Logical.AND) + { + checkRoleAnd(requiresRoles.value()); + } + else + { + checkRoleOr(requiresRoles.value()); + } + } + + /** + * 验证用户是否含有指定角色,必须全部拥有 + * + * @param roles 角色标识数组 + */ + public void checkRoleAnd(String... roles) + { + Set roleList = getRoleList(); + for (String role : roles) + { + if (!hasRole(roleList, role)) + { + throw new NotRoleException(role); + } + } + } + + /** + * 验证用户是否含有指定角色,只需包含其中一个 + * + * @param roles 角色标识数组 + */ + public void checkRoleOr(String... roles) + { + Set roleList = getRoleList(); + for (String role : roles) + { + if (hasRole(roleList, role)) + { + return; + } + } + if (roles.length > 0) + { + throw new NotRoleException(roles); + } + } + + /** + * 根据注解(@RequiresLogin)鉴权 + * + * @param at 注解对象 + */ + public void checkByAnnotation(RequiresLogin at) + { + this.checkLogin(); + } + + /** + * 根据注解(@RequiresRoles)鉴权 + * + * @param at 注解对象 + */ + public void checkByAnnotation(RequiresRoles at) + { + String[] roleArray = at.value(); + if (at.logical() == Logical.AND) + { + this.checkRoleAnd(roleArray); + } + else + { + this.checkRoleOr(roleArray); + } + } + + /** + * 根据注解(@RequiresPermissions)鉴权 + * + * @param at 注解对象 + */ + public void checkByAnnotation(RequiresPermissions at) + { + String[] permissionArray = at.value(); + if (at.logical() == Logical.AND) + { + this.checkPermiAnd(permissionArray); + } + else + { + this.checkPermiOr(permissionArray); + } + } + + /** + * 获取当前账号的角色列表 + * + * @return 角色列表 + */ + public Set getRoleList() + { + try + { + LoginUser loginUser = getLoginUser(); + return loginUser.getRoles(); + } + catch (Exception e) + { + return new HashSet<>(); + } + } + + /** + * 获取当前账号的权限列表 + * + * @return 权限列表 + */ + public Set getPermiList() + { + try + { + LoginUser loginUser = getLoginUser(); + return loginUser.getPermissions(); + } + catch (Exception e) + { + return new HashSet<>(); + } + } + + /** + * 判断是否包含权限 + * + * @param authorities 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public boolean hasPermi(Collection authorities, String permission) + { + return authorities.stream().filter(StringUtils::hasText) + .anyMatch(x -> ALL_PERMISSION.equals(x) || PatternMatchUtils.simpleMatch(x, permission)); + } + + /** + * 判断是否包含角色 + * + * @param roles 角色列表 + * @param role 角色 + * @return 用户是否具备某角色权限 + */ + public boolean hasRole(Collection roles, String role) + { + return roles.stream().filter(StringUtils::hasText) + .anyMatch(x -> SUPER_ADMIN.equals(x) || PatternMatchUtils.simpleMatch(x, role)); + } +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/auth/AuthUtil.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/auth/AuthUtil.java new file mode 100644 index 0000000..5d5c6d8 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/auth/AuthUtil.java @@ -0,0 +1,167 @@ +package com.storm.common.security.auth; + +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.annotation.RequiresRoles; +import com.storm.system.api.model.LoginUser; + +/** + * Token 权限验证工具类 + * + * @author ruoyi + */ +public class AuthUtil +{ + /** + * 底层的 AuthLogic 对象 + */ + public static AuthLogic authLogic = new AuthLogic(); + + /** + * 会话注销 + */ + public static void logout() + { + authLogic.logout(); + } + + /** + * 会话注销,根据指定Token + * + * @param token 指定token + */ + public static void logoutByToken(String token) + { + authLogic.logoutByToken(token); + } + + /** + * 检验当前会话是否已经登录,如未登录,则抛出异常 + */ + public static void checkLogin() + { + authLogic.checkLogin(); + } + + /** + * 获取当前登录用户信息 + * + * @param token 指定token + * @return 用户信息 + */ + public static LoginUser getLoginUser(String token) + { + return authLogic.getLoginUser(token); + } + + /** + * 验证当前用户有效期 + * + * @param loginUser 用户信息 + */ + public static void verifyLoginUserExpire(LoginUser loginUser) + { + authLogic.verifyLoginUserExpire(loginUser); + } + + /** + * 当前账号是否含有指定角色标识, 返回true或false + * + * @param role 角色标识 + * @return 是否含有指定角色标识 + */ + public static boolean hasRole(String role) + { + return authLogic.hasRole(role); + } + + /** + * 当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException + * + * @param role 角色标识 + */ + public static void checkRole(String role) + { + authLogic.checkRole(role); + } + + /** + * 根据注解传入参数鉴权, 如果验证未通过,则抛出异常: NotRoleException + * + * @param requiresRoles 角色权限注解 + */ + public static void checkRole(RequiresRoles requiresRoles) + { + authLogic.checkRole(requiresRoles); + } + + /** + * 当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] + * + * @param roles 角色标识数组 + */ + public static void checkRoleAnd(String... roles) + { + authLogic.checkRoleAnd(roles); + } + + /** + * 当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] + * + * @param roles 角色标识数组 + */ + public static void checkRoleOr(String... roles) + { + authLogic.checkRoleOr(roles); + } + + /** + * 当前账号是否含有指定权限, 返回true或false + * + * @param permission 权限码 + * @return 是否含有指定权限 + */ + public static boolean hasPermi(String permission) + { + return authLogic.hasPermi(permission); + } + + /** + * 当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException + * + * @param permission 权限码 + */ + public static void checkPermi(String permission) + { + authLogic.checkPermi(permission); + } + + /** + * 根据注解传入参数鉴权, 如果验证未通过,则抛出异常: NotPermissionException + * + * @param requiresPermissions 权限注解 + */ + public static void checkPermi(RequiresPermissions requiresPermissions) + { + authLogic.checkPermi(requiresPermissions); + } + + /** + * 当前账号是否含有指定权限 [指定多个,必须全部验证通过] + * + * @param permissions 权限码数组 + */ + public static void checkPermiAnd(String... permissions) + { + authLogic.checkPermiAnd(permissions); + } + + /** + * 当前账号是否含有指定权限 [指定多个,只要其一验证通过即可] + * + * @param permissions 权限码数组 + */ + public static void checkPermiOr(String... permissions) + { + authLogic.checkPermiOr(permissions); + } +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/ApplicationConfig.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/ApplicationConfig.java new file mode 100644 index 0000000..f8ff893 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/ApplicationConfig.java @@ -0,0 +1,22 @@ +package com.storm.common.security.config; + +import java.util.TimeZone; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; + +/** + * 系统配置 + * + * @author ruoyi + */ +public class ApplicationConfig +{ + /** + * 时区配置 + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() + { + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault()); + } +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/MetaObjectHandlerConfig.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/MetaObjectHandlerConfig.java new file mode 100644 index 0000000..fa6756b --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/MetaObjectHandlerConfig.java @@ -0,0 +1,14 @@ +package com.storm.common.security.config; + +import com.storm.common.security.interceptor.MyMetaObjectHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MetaObjectHandlerConfig { + + @Bean + public MyMetaObjectHandler metaObjectHandler() { + return new MyMetaObjectHandler(); + } +} \ No newline at end of file diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/MybatisPlusConfig.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/MybatisPlusConfig.java new file mode 100644 index 0000000..110b9e7 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/MybatisPlusConfig.java @@ -0,0 +1,59 @@ +package com.storm.common.security.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * Mybatis Plus 配置 + * + * @author ruoyi + */ +@EnableTransactionManagement(proxyTargetClass = true) +@Configuration +public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 分页插件 + interceptor.addInnerInterceptor(paginationInnerInterceptor()); + // 乐观锁插件 + interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor()); + // 阻断插件 + interceptor.addInnerInterceptor(blockAttackInnerInterceptor()); + return interceptor; + } + + /** + * 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html + */ + public PaginationInnerInterceptor paginationInnerInterceptor() { + PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); + // 设置数据库类型为mysql + paginationInnerInterceptor.setDbType(DbType.MYSQL); + // 设置最大单页限制数量,默认 500 条,-1 不受限制 + paginationInnerInterceptor.setMaxLimit(-1L); + return paginationInnerInterceptor; + } + + /** + * 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html + */ + public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() { + return new OptimisticLockerInnerInterceptor(); + } + + /** + * 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html + */ + public BlockAttackInnerInterceptor blockAttackInnerInterceptor() { + return new BlockAttackInnerInterceptor(); + } + +} \ No newline at end of file diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/WebMvcConfig.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/WebMvcConfig.java new file mode 100644 index 0000000..5fcf435 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/config/WebMvcConfig.java @@ -0,0 +1,33 @@ +package com.storm.common.security.config; + +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.storm.common.security.interceptor.HeaderInterceptor; + +/** + * 拦截器配置 + * + * @author ruoyi + */ +public class WebMvcConfig implements WebMvcConfigurer +{ + /** 不需要拦截地址 */ + public static final String[] excludeUrls = { "/login", "/logout", "/refresh" }; + + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(getHeaderInterceptor()) + .addPathPatterns("/**") + .excludePathPatterns(excludeUrls) + .order(-10); + } + + /** + * 自定义请求头拦截器 + */ + public HeaderInterceptor getHeaderInterceptor() + { + return new HeaderInterceptor(); + } +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/feign/FeignAutoConfiguration.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/feign/FeignAutoConfiguration.java new file mode 100644 index 0000000..724a45e --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/feign/FeignAutoConfiguration.java @@ -0,0 +1,20 @@ +package com.storm.common.security.feign; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import feign.RequestInterceptor; + +/** + * Feign 配置注册 + * + * @author ruoyi + **/ +@Configuration +public class FeignAutoConfiguration +{ + @Bean + public RequestInterceptor requestInterceptor() + { + return new FeignRequestInterceptor(); + } +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/feign/FeignRequestInterceptor.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/feign/FeignRequestInterceptor.java new file mode 100644 index 0000000..c3b0257 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/feign/FeignRequestInterceptor.java @@ -0,0 +1,54 @@ +package com.storm.common.security.feign; + +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.utils.ServletUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.ip.IpUtils; +import feign.RequestInterceptor; +import feign.RequestTemplate; + +/** + * feign 请求拦截器 + * + * @author ruoyi + */ +@Component +public class FeignRequestInterceptor implements RequestInterceptor +{ + @Override + public void apply(RequestTemplate requestTemplate) + { + HttpServletRequest httpServletRequest = ServletUtils.getRequest(); + if (StringUtils.isNotNull(httpServletRequest)) + { + Map headers = ServletUtils.getHeaders(httpServletRequest); + // 传递用户信息请求头,防止丢失 + String userId = headers.get(SecurityConstants.DETAILS_USER_ID); + if (StringUtils.isNotEmpty(userId)) + { + requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId); + } + String userKey = headers.get(SecurityConstants.USER_KEY); + if (StringUtils.isNotEmpty(userKey)) + { + requestTemplate.header(SecurityConstants.USER_KEY, userKey); + } + String userName = headers.get(SecurityConstants.DETAILS_USERNAME); + if (StringUtils.isNotEmpty(userName)) + { + requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName); + } + String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER); + if (StringUtils.isNotEmpty(authentication)) + { + requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication); + } + + // 配置客户端IP + requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr()); + } + } +} \ No newline at end of file diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/handler/GlobalExceptionHandler.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..c6805a2 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/handler/GlobalExceptionHandler.java @@ -0,0 +1,166 @@ +package com.storm.common.security.handler; + +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.storm.common.core.constant.HttpStatus; +import com.storm.common.core.exception.DemoModeException; +import com.storm.common.core.exception.InnerAuthException; +import com.storm.common.core.exception.ServiceException; +import com.storm.common.core.exception.auth.NotPermissionException; +import com.storm.common.core.exception.auth.NotRoleException; +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.html.EscapeUtil; +import com.storm.common.core.web.domain.AjaxResult; + +/** + * 全局异常处理器 + * + * @author ruoyi + */ +@RestControllerAdvice +public class GlobalExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 权限码异常 + */ + @ExceptionHandler(NotPermissionException.class) + public AjaxResult handleNotPermissionException(NotPermissionException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',权限码校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.FORBIDDEN, "没有访问权限,请联系管理员授权"); + } + + /** + * 角色权限异常 + */ + @ExceptionHandler(NotRoleException.class) + public AjaxResult handleNotRoleException(NotRoleException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',角色权限校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.FORBIDDEN, "没有访问权限,请联系管理员授权"); + } + + /** + * 请求方式不支持 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); + return AjaxResult.error(e.getMessage()); + } + + /** + * 业务异常 + */ + @ExceptionHandler(ServiceException.class) + public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) + { + log.error(e.getMessage(), e); + Integer code = e.getCode(); + return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); + } + + /** + * 请求路径中缺少必需的路径变量 + */ + @ExceptionHandler(MissingPathVariableException.class) + public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName())); + } + + /** + * 请求参数类型不匹配 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + String value = Convert.toStr(e.getValue()); + if (StringUtils.isNotEmpty(value)) + { + value = EscapeUtil.clean(value); + } + log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value)); + } + + /** + * 拦截未知的运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生未知异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 系统异常 + */ + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public AjaxResult handleBindException(BindException e) + { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) + { + log.error(e.getMessage(), e); + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 内部认证异常 + */ + @ExceptionHandler(InnerAuthException.class) + public AjaxResult handleInnerAuthException(InnerAuthException e) + { + return AjaxResult.error(e.getMessage()); + } + + /** + * 演示模式异常 + */ + @ExceptionHandler(DemoModeException.class) + public AjaxResult handleDemoModeException(DemoModeException e) + { + return AjaxResult.error("演示模式,不允许操作"); + } +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/interceptor/HeaderInterceptor.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/interceptor/HeaderInterceptor.java new file mode 100644 index 0000000..605abe4 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/interceptor/HeaderInterceptor.java @@ -0,0 +1,54 @@ +package com.storm.common.security.interceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.context.SecurityContextHolder; +import com.storm.common.core.utils.ServletUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.security.auth.AuthUtil; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.model.LoginUser; + +/** + * 自定义请求头拦截器,将Header数据封装到线程变量中方便获取 + * 注意:此拦截器会同时验证当前用户有效期自动刷新有效期 + * + * @author ruoyi + */ +public class HeaderInterceptor implements AsyncHandlerInterceptor +{ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + if (!(handler instanceof HandlerMethod)) + { + return true; + } + + SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID)); + SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME)); + SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY)); + + String token = SecurityUtils.getToken(); + if (StringUtils.isNotEmpty(token)) + { + LoginUser loginUser = AuthUtil.getLoginUser(token); + if (StringUtils.isNotNull(loginUser)) + { + AuthUtil.verifyLoginUserExpire(loginUser); + SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser); + } + } + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception + { + SecurityContextHolder.remove(); + } +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/interceptor/MyMetaObjectHandler.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/interceptor/MyMetaObjectHandler.java new file mode 100644 index 0000000..ff6a427 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/interceptor/MyMetaObjectHandler.java @@ -0,0 +1,62 @@ +package com.storm.common.security.interceptor; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.model.LoginUser; +import lombok.SneakyThrows; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Date; + +@Component +public class MyMetaObjectHandler implements MetaObjectHandler { + + @Resource + private HttpServletRequest request; + + @SneakyThrows + public boolean isExclude() { + try { + String requestURI = request.getRequestURI(); + if (requestURI.startsWith("/member")) { + return true; + } + return false; + } catch (Exception e) { + return true; + } + } + + @Override + public void insertFill(MetaObject metaObject) { + this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); + // 自动填充创建人 + if (!isExclude()) { + this.strictInsertFill(metaObject, "createBy", String.class, String.valueOf(loadUserId())); + } + } + + @Override + public void updateFill(MetaObject metaObject) { + this.setFieldValByName("updateTime", new Date(), metaObject); + if (!isExclude()) { + this.setFieldValByName("updateBy", String.valueOf(loadUserId()), metaObject); + } + } + + /** + * 获取当前登录人的ID + * + * @return 登录人ID + */ + public Long loadUserId() { + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser != null) { + return loginUser.getUserid(); + } + return 1L; + } +} \ No newline at end of file diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/service/TokenService.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/service/TokenService.java new file mode 100644 index 0000000..2050131 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/service/TokenService.java @@ -0,0 +1,174 @@ +package com.storm.common.security.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.storm.common.core.constant.CacheConstants; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.utils.JwtUtils; +import com.storm.common.core.utils.ServletUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.ip.IpUtils; +import com.storm.common.core.utils.uuid.IdUtils; +import com.storm.common.redis.service.RedisService; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.model.LoginUser; + +/** + * token验证处理 + * + * @author ruoyi + */ +@Component +public class TokenService +{ + private static final Logger log = LoggerFactory.getLogger(TokenService.class); + + @Autowired + private RedisService redisService; + + protected static final long MILLIS_SECOND = 1000; + + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + private final static long TOKEN_EXPIRE_TIME = CacheConstants.EXPIRATION; + + private final static String ACCESS_TOKEN = CacheConstants.LOGIN_TOKEN_KEY; + + private final static Long TOKEN_REFRESH_THRESHOLD_MINUTES = CacheConstants.REFRESH_TIME * MILLIS_MINUTE; + + /** + * 创建令牌 + */ + public Map createToken(LoginUser loginUser) + { + String token = IdUtils.fastUUID(); + Long userId = loginUser.getSysUser().getUserId(); + String userName = loginUser.getSysUser().getUserName(); + loginUser.setToken(token); + loginUser.setUserid(userId); + loginUser.setUsername(userName); + loginUser.setIpaddr(IpUtils.getIpAddr()); + refreshToken(loginUser); + + // Jwt存储信息 + Map claimsMap = new HashMap(); + claimsMap.put(SecurityConstants.USER_KEY, token); + claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId); + claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName); + + // 接口返回信息 + Map rspMap = new HashMap(); + rspMap.put("access_token", JwtUtils.createToken(claimsMap)); + rspMap.put("expires_in", TOKEN_EXPIRE_TIME); + return rspMap; + } + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser() + { + return getLoginUser(ServletUtils.getRequest()); + } + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(HttpServletRequest request) + { + // 获取请求携带的令牌 + String token = SecurityUtils.getToken(request); + return getLoginUser(token); + } + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(String token) + { + LoginUser user = null; + try + { + if (StringUtils.isNotEmpty(token)) + { + String userkey = JwtUtils.getUserKey(token); + user = redisService.getCacheObject(getTokenKey(userkey)); + return user; + } + } + catch (Exception e) + { + log.error("获取用户信息异常'{}'", e.getMessage()); + } + return user; + } + + /** + * 设置用户身份信息 + */ + public void setLoginUser(LoginUser loginUser) + { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) + { + refreshToken(loginUser); + } + } + + /** + * 删除用户缓存信息 + */ + public void delLoginUser(String token) + { + if (StringUtils.isNotEmpty(token)) + { + String userkey = JwtUtils.getUserKey(token); + redisService.deleteObject(getTokenKey(userkey)); + } + } + + /** + * 验证令牌有效期,相差不足120分钟,自动刷新缓存 + * + * @param loginUser + */ + public void verifyToken(LoginUser loginUser) + { + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= TOKEN_REFRESH_THRESHOLD_MINUTES) + { + refreshToken(loginUser); + } + } + + /** + * 刷新令牌有效期 + * + * @param loginUser 登录信息 + */ + public void refreshToken(LoginUser loginUser) + { + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(loginUser.getLoginTime() + TOKEN_EXPIRE_TIME * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(loginUser.getToken()); + redisService.setCacheObject(userKey, loginUser, TOKEN_EXPIRE_TIME, TimeUnit.MINUTES); + } + + private String getTokenKey(String token) + { + return ACCESS_TOKEN + token; + } +} \ No newline at end of file diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/utils/DictUtils.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/utils/DictUtils.java new file mode 100644 index 0000000..95e0d12 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/utils/DictUtils.java @@ -0,0 +1,75 @@ +package com.storm.common.security.utils; + +import java.util.Collection; +import java.util.List; +import com.alibaba.fastjson2.JSONArray; +import com.storm.common.core.constant.CacheConstants; +import com.storm.common.core.utils.SpringUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.redis.service.RedisService; +import com.storm.system.api.domain.SysDictData; + +/** + * 字典工具类 + * + * @author ruoyi + */ +public class DictUtils +{ + /** + * 设置字典缓存 + * + * @param key 参数键 + * @param dictDatas 字典数据列表 + */ + public static void setDictCache(String key, List dictDatas) + { + SpringUtils.getBean(RedisService.class).setCacheObject(getCacheKey(key), dictDatas); + } + + /** + * 获取字典缓存 + * + * @param key 参数键 + * @return dictDatas 字典数据列表 + */ + public static List getDictCache(String key) + { + JSONArray arrayCache = SpringUtils.getBean(RedisService.class).getCacheObject(getCacheKey(key)); + if (StringUtils.isNotNull(arrayCache)) + { + return arrayCache.toList(SysDictData.class); + } + return null; + } + + /** + * 删除指定字典缓存 + * + * @param key 字典键 + */ + public static void removeDictCache(String key) + { + SpringUtils.getBean(RedisService.class).deleteObject(getCacheKey(key)); + } + + /** + * 清空字典缓存 + */ + public static void clearDictCache() + { + Collection keys = SpringUtils.getBean(RedisService.class).keys(CacheConstants.SYS_DICT_KEY + "*"); + SpringUtils.getBean(RedisService.class).deleteObject(keys); + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + public static String getCacheKey(String configKey) + { + return CacheConstants.SYS_DICT_KEY + configKey; + } +} diff --git a/storm-common/storm-common-security/src/main/java/com/storm/common/security/utils/SecurityUtils.java b/storm-common/storm-common-security/src/main/java/com/storm/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000..e55a156 --- /dev/null +++ b/storm-common/storm-common-security/src/main/java/com/storm/common/security/utils/SecurityUtils.java @@ -0,0 +1,117 @@ +package com.storm.common.security.utils; + +import javax.servlet.http.HttpServletRequest; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.constant.TokenConstants; +import com.storm.common.core.context.SecurityContextHolder; +import com.storm.common.core.utils.ServletUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.system.api.model.LoginUser; + +/** + * 权限获取工具类 + * + * @author ruoyi + */ +public class SecurityUtils +{ + /** + * 获取用户ID + */ + public static Long getUserId() + { + return SecurityContextHolder.getUserId(); + } + + /** + * 获取用户名称 + */ + public static String getUsername() + { + return SecurityContextHolder.getUserName(); + } + + /** + * 获取用户key + */ + public static String getUserKey() + { + return SecurityContextHolder.getUserKey(); + } + + /** + * 获取登录用户信息 + */ + public static LoginUser getLoginUser() + { + return SecurityContextHolder.get(SecurityConstants.LOGIN_USER, LoginUser.class); + } + + /** + * 获取请求token + */ + public static String getToken() + { + return getToken(ServletUtils.getRequest()); + } + + /** + * 根据request获取请求token + */ + public static String getToken(HttpServletRequest request) + { + // 从header获取token标识 + String token = request.getHeader(SecurityConstants.AUTHORIZATION_HEADER); + return replaceTokenPrefix(token); + } + + /** + * 裁剪token前缀 + */ + public static String replaceTokenPrefix(String token) + { + // 如果前端设置了令牌前缀,则裁剪掉前缀 + if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) + { + token = token.replaceFirst(TokenConstants.PREFIX, ""); + } + return token; + } + + /** + * 是否为管理员 + * + * @param userId 用户ID + * @return 结果 + */ + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } + + /** + * 判断密码是否相同 + * + * @param rawPassword 真实密码 + * @param encodedPassword 加密后字符 + * @return 结果 + */ + public static boolean matchesPassword(String rawPassword, String encodedPassword) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.matches(rawPassword, encodedPassword); + } +} diff --git a/storm-common/storm-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/storm-common/storm-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..3850746 --- /dev/null +++ b/storm-common/storm-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +com.storm.common.security.config.WebMvcConfig +com.storm.common.security.service.TokenService +com.storm.common.security.aspect.PreAuthorizeAspect +com.storm.common.security.aspect.InnerAuthAspect +com.storm.common.security.handler.GlobalExceptionHandler +com.storm.common.security.config.MybatisPlusConfig +com.storm.common.security.config.MetaObjectHandlerConfig \ No newline at end of file diff --git a/storm-common/storm-common-sensitive/pom.xml b/storm-common/storm-common-sensitive/pom.xml new file mode 100644 index 0000000..f1b6770 --- /dev/null +++ b/storm-common/storm-common-sensitive/pom.xml @@ -0,0 +1,27 @@ + + + + com.storm + storm-common + 3.6.6 + + 4.0.0 + + storm-common-sensitive + + + storm-common-sensitive数据脱敏 + + + + + + + com.storm + storm-common-core + + + + \ No newline at end of file diff --git a/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/annotation/Sensitive.java b/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/annotation/Sensitive.java new file mode 100644 index 0000000..ff78096 --- /dev/null +++ b/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/annotation/Sensitive.java @@ -0,0 +1,24 @@ +package com.storm.common.sensitive.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.storm.common.sensitive.config.SensitiveJsonSerializer; +import com.storm.common.sensitive.enums.DesensitizedType; + +/** + * 数据脱敏注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@JacksonAnnotationsInside +@JsonSerialize(using = SensitiveJsonSerializer.class) +public @interface Sensitive +{ + DesensitizedType desensitizedType(); +} diff --git a/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/config/SensitiveJsonSerializer.java b/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/config/SensitiveJsonSerializer.java new file mode 100644 index 0000000..ea46a71 --- /dev/null +++ b/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/config/SensitiveJsonSerializer.java @@ -0,0 +1,67 @@ +package com.storm.common.sensitive.config; + +import java.io.IOException; +import java.util.Objects; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.context.SecurityContextHolder; +import com.storm.common.sensitive.annotation.Sensitive; +import com.storm.common.sensitive.enums.DesensitizedType; + +/** + * 数据脱敏序列化过滤 + * + * @author ruoyi + */ +public class SensitiveJsonSerializer extends JsonSerializer implements ContextualSerializer +{ + private DesensitizedType desensitizedType; + + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException + { + if (desensitization()) + { + gen.writeString(desensitizedType.desensitizer().apply(value)); + } + else + { + gen.writeString(value); + } + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) + throws JsonMappingException + { + Sensitive annotation = property.getAnnotation(Sensitive.class); + if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) + { + this.desensitizedType = annotation.desensitizedType(); + return this; + } + return prov.findValueSerializer(property.getType(), property); + } + + /** + * 是否需要脱敏处理 + */ + private boolean desensitization() + { + try + { + Long userId = SecurityContextHolder.getUserId(); + // 管理员不脱敏 + return !UserConstants.isAdmin(userId); + } + catch (Exception e) + { + return true; + } + } +} diff --git a/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/enums/DesensitizedType.java b/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/enums/DesensitizedType.java new file mode 100644 index 0000000..c27e505 --- /dev/null +++ b/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/enums/DesensitizedType.java @@ -0,0 +1,59 @@ +package com.storm.common.sensitive.enums; + +import java.util.function.Function; +import com.storm.common.sensitive.utils.DesensitizedUtil; + +/** + * 脱敏类型 + * + * @author ruoyi + */ +public enum DesensitizedType +{ + /** + * 姓名,第2位星号替换 + */ + USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")), + + /** + * 密码,全部字符都用*代替 + */ + PASSWORD(DesensitizedUtil::password), + + /** + * 身份证,中间10位星号替换 + */ + ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\d{3}[Xx]|\\d{4})", "$1** **** ****$2")), + + /** + * 手机号,中间4位星号替换 + */ + PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")), + + /** + * 电子邮箱,仅显示第一个字母和@后面的地址显示,其他星号替换 + */ + EMAIL(s -> s.replaceAll("(^.)[^@]*(@.*$)", "$1****$2")), + + /** + * 银行卡号,保留最后4位,其他星号替换 + */ + BANK_CARD(s -> s.replaceAll("\\d{15}(\\d{3})", "**** **** **** **** $1")), + + /** + * 车牌号码,包含普通车辆、新能源车辆 + */ + CAR_LICENSE(DesensitizedUtil::carLicense); + + private final Function desensitizer; + + DesensitizedType(Function desensitizer) + { + this.desensitizer = desensitizer; + } + + public Function desensitizer() + { + return desensitizer; + } +} diff --git a/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/utils/DesensitizedUtil.java b/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/utils/DesensitizedUtil.java new file mode 100644 index 0000000..ed67dbc --- /dev/null +++ b/storm-common/storm-common-sensitive/src/main/java/com/ruoyi/common/sensitive/utils/DesensitizedUtil.java @@ -0,0 +1,51 @@ +package com.storm.common.sensitive.utils; + +import com.storm.common.core.utils.StringUtils; + +/** + * 脱敏工具类 + * + * @author ruoyi + */ +public class DesensitizedUtil +{ + /** + * 密码的全部字符都用*代替,比如:****** + * + * @param password 密码 + * @return 脱敏后的密码 + */ + public static String password(String password) + { + if (StringUtils.isBlank(password)) + { + return StringUtils.EMPTY; + } + return StringUtils.repeat('*', password.length()); + } + + /** + * 车牌中间用*代替,如果是错误的车牌,不处理 + * + * @param carLicense 完整的车牌号 + * @return 脱敏后的车牌 + */ + public static String carLicense(String carLicense) + { + if (StringUtils.isBlank(carLicense)) + { + return StringUtils.EMPTY; + } + // 普通车牌 + if (carLicense.length() == 7) + { + carLicense = StringUtils.hide(carLicense, 3, 6); + } + else if (carLicense.length() == 8) + { + // 新能源车牌 + carLicense = StringUtils.hide(carLicense, 3, 7); + } + return carLicense; + } +} diff --git a/storm-common/storm-common-swagger/pom.xml b/storm-common/storm-common-swagger/pom.xml new file mode 100644 index 0000000..dde9ef3 --- /dev/null +++ b/storm-common/storm-common-swagger/pom.xml @@ -0,0 +1,37 @@ + + + + com.storm + storm-common + 3.6.6 + + 4.0.0 + + storm-common-swagger + + + storm-common-swagger系统接口 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.github.xiaoymin + knife4j-openapi3-spring-boot-starter + + + + org.projectlombok + lombok + + + diff --git a/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/Knife4jConfiguration.java b/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/Knife4jConfiguration.java new file mode 100644 index 0000000..40c0944 --- /dev/null +++ b/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/Knife4jConfiguration.java @@ -0,0 +1,121 @@ +package com.storm.common.swagger.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; + +@Configuration +@ConditionalOnProperty(prefix = "storm.swagger", name = "enable", havingValue = "true") +@EnableConfigurationProperties(SwaggerConfigProperties.class) +public class Knife4jConfiguration { + + @Resource + private SwaggerConfigProperties swaggerConfigProperties; + + @Bean + @ConditionalOnProperty(name = "storm.swagger.enable", havingValue = "true") + public OpenAPI customOpenAPI() { + OpenAPI openAPI = new OpenAPI() + .info(new Info() + .title(this.swaggerConfigProperties.getTitle()) + .version(this.swaggerConfigProperties.getVersion()) + .description(this.swaggerConfigProperties.getDescription()) + .contact(new Contact().name(this.swaggerConfigProperties.getContactName()) + .url(this.swaggerConfigProperties.getContactUrl()) + .email(this.swaggerConfigProperties.getContactEmail())) + ); + // 添加安全配置 + openAPI.components(new io.swagger.v3.oas.models.Components() + .addSecuritySchemes("apikey", securityScheme())) + .addSecurityItem(new SecurityRequirement().addList("apikey")); + + // 添加服务器配置 + if (StringUtils.hasText(swaggerConfigProperties.getGatewayUrl())) { + openAPI.servers(servers(swaggerConfigProperties.getGatewayUrl())); + } + + return openAPI; + } + + // 添加包扫描配置 - 这是关键修复 + @Bean + @ConditionalOnProperty(name = "storm.swagger.enable", havingValue = "true") + public GroupedOpenApi groupedOpenApi() { + return GroupedOpenApi.builder() + .group("default") + .packagesToScan(swaggerConfigProperties.getPackagePath()) + .build(); + } + + // 安全配置 + public SecurityScheme securityScheme() { + return new SecurityScheme().type(SecurityScheme.Type.APIKEY) + .name("Authorization") + .in(SecurityScheme.In.HEADER) + .scheme("Bearer"); + } + + // 服务器配置 + public List servers(String gatewayUrl) { + List serverList = new ArrayList<>(); + serverList.add(new Server().url(gatewayUrl)); + return serverList; + } + + + // @Bean(value = "defaultApi2") + // public Docket defaultApi2(TypeResolver typeResolver) { + // // 1.初始化Docket + // Docket docket = new Docket(DocumentationType.SWAGGER_2); + // // 2.是否需要包装R + // if(swaggerConfigProperties.getEnableResponseWrap()){ + // docket.additionalModels(typeResolver.resolve(R.class)); + // } + // return docket.apiInfo(new ApiInfoBuilder() + // .title(this.swaggerConfigProperties.getTitle()) + // .description(this.swaggerConfigProperties.getDescription()) + // .contact(new Contact( + // this.swaggerConfigProperties.getContactName(), + // this.swaggerConfigProperties.getContactUrl(), + // this.swaggerConfigProperties.getContactEmail())) + // .version(this.swaggerConfigProperties.getVersion()) + // .build()) + // .select() + // //这里指定Controller扫描包路径 + // .apis(RequestHandlerSelectors.basePackage(swaggerConfigProperties.getPackagePath())) + // .paths(PathSelectors.any()) + // .build(); + // + // } + // @Bean + // @Primary + // @ConditionalOnProperty(prefix = "tj.swagger", name = "enableResponseWrap",havingValue = "true") + // public BaseSwaggerResponseModelPlugin baseSwaggerResponseModelPlugin(){ + // return new BaseSwaggerResponseModelPlugin(); + // } + // @Bean + // @Primary + // @ConditionalOnProperty(prefix = "tj.swagger", name = "enableResponseWrap",havingValue = "true") + // public BaseSwaggerResponseBuilderPlugin baseSwaggerResponseBuilderPlugin(){ + // return new BaseSwaggerResponseBuilderPlugin(); + // } + // { + // // hutool的日期转换器加载 + // ConverterRegistry converterRegistry = ConverterRegistry.getInstance(); + // converterRegistry.putCustom(LocalDateTime.class, new TjTemporalConverter(LocalDateTime.class)); + // } + +} diff --git a/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/SpringDocAutoConfiguration.java b/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/SpringDocAutoConfiguration.java new file mode 100644 index 0000000..52baf41 --- /dev/null +++ b/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/SpringDocAutoConfiguration.java @@ -0,0 +1,67 @@ +/* +package com.storm.common.swagger.config; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import com.storm.common.swagger.config.properties.SpringDocProperties; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; + +*/ +/** + * Swagger 文档配置 + * + * @author ruoyi + *//* + +@EnableConfigurationProperties(SpringDocProperties.class) +@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true) +public class SpringDocAutoConfiguration +{ + @Bean + @ConditionalOnMissingBean(OpenAPI.class) + public OpenAPI openApi(SpringDocProperties properties) + { + return new OpenAPI().components(new Components() + // 设置认证的请求头 + .addSecuritySchemes("apikey", securityScheme())) + .addSecurityItem(new SecurityRequirement().addList("apikey")) + .info(convertInfo(properties.getInfo())) + .servers(servers(properties.getGatewayUrl())); + } + + public SecurityScheme securityScheme() + { + return new SecurityScheme().type(SecurityScheme.Type.APIKEY) + .name("Authorization") + .in(SecurityScheme.In.HEADER) + .scheme("Bearer"); + } + + private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) + { + Info info = new Info(); + info.setTitle(infoProperties.getTitle()); + info.setDescription(infoProperties.getDescription()); + info.setContact(infoProperties.getContact()); + info.setLicense(infoProperties.getLicense()); + info.setVersion(infoProperties.getVersion()); + return info; + } + + public List servers(String gatewayUrl) + { + List serverList = new ArrayList<>(); + serverList.add(new Server().url(gatewayUrl)); + return serverList; + } +} +*/ diff --git a/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/SwaggerConfigProperties.java b/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/SwaggerConfigProperties.java new file mode 100644 index 0000000..878f07c --- /dev/null +++ b/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/SwaggerConfigProperties.java @@ -0,0 +1,32 @@ +package com.storm.common.swagger.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.io.Serializable; + + +@Data +@ConfigurationProperties(prefix = "storm.swagger") +public class SwaggerConfigProperties implements Serializable { + + private Boolean enable = false; + private Boolean enableResponseWrap = false; + + public String packagePath; + + public String title; + + public String description; + + public String contactName; + + public String contactUrl; + + public String contactEmail; + + public String version; + + // 新增属性,用于替代原 SpringDoc 配置 + private String gatewayUrl; +} diff --git a/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/properties/SpringDocProperties.java b/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/properties/SpringDocProperties.java new file mode 100644 index 0000000..1447594 --- /dev/null +++ b/storm-common/storm-common-swagger/src/main/java/com/storm/common/swagger/config/properties/SpringDocProperties.java @@ -0,0 +1,155 @@ +/* +package com.storm.common.swagger.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.License; + +*/ +/** + * Swagger 配置属性 + * + * @author ruoyi + *//* + +@ConfigurationProperties(prefix = "springdoc") +public class SpringDocProperties +{ + */ +/** + * 网关 + *//* + + private String gatewayUrl; + + */ +/** + * 文档基本信息 + *//* + + @NestedConfigurationProperty + private InfoProperties info = new InfoProperties(); + + */ +/** + *

+ * 文档的基础属性信息 + *

+ * + * @see io.swagger.v3.oas.models.info.Info + * + * 为了 springboot 自动生产配置提示信息,所以这里复制一个类出来 + *//* + + public static class InfoProperties + { + */ +/** + * 标题 + *//* + + private String title = null; + + */ +/** + * 描述 + *//* + + private String description = null; + + */ +/** + * 联系人信息 + *//* + + @NestedConfigurationProperty + private Contact contact = null; + + */ +/** + * 许可证 + *//* + + @NestedConfigurationProperty + private License license = null; + + */ +/** + * 版本 + *//* + + private String version = null; + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + } + + public Contact getContact() + { + return contact; + } + + public void setContact(Contact contact) + { + this.contact = contact; + } + + public License getLicense() + { + return license; + } + + public void setLicense(License license) + { + this.license = license; + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + } + + public String getGatewayUrl() + { + return gatewayUrl; + } + + public void setGatewayUrl(String gatewayUrl) + { + this.gatewayUrl = gatewayUrl; + } + + public InfoProperties getInfo() + { + return info; + } + + public void setInfo(InfoProperties info) + { + this.info = info; + } +} +*/ diff --git a/storm-common/storm-common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/storm-common/storm-common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..da752e5 --- /dev/null +++ b/storm-common/storm-common-swagger/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.storm.common.swagger.config.Knife4jConfiguration \ No newline at end of file diff --git a/storm-device/pom.xml b/storm-device/pom.xml new file mode 100644 index 0000000..c0300f8 --- /dev/null +++ b/storm-device/pom.xml @@ -0,0 +1,135 @@ + + + 4.0.0 + + com.storm + storm + 3.6.6 + + + storm-device + + + 11 + 11 + UTF-8 + + + + storm-device设备管理 + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + 2.2.6.RELEASE + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + 2.2.6.RELEASE + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + + com.storm + storm-common-security + + + + com.storm + storm-common-redis + + + + com.storm + storm-common-datascope + + + + com.storm + storm-api-system + + + + + com.mysql + mysql-connector-j + + + + + com.storm + storm-common-log + + + + + com.storm + storm-common-swagger + + + + org.projectlombok + lombok + + + + com.storm + storm-common-oss + 3.6.6 + + + + + com.huaweicloud.sdk + huaweicloud-sdk-core + + + com.huaweicloud.sdk + huaweicloud-sdk-iotda + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.apache.qpid + qpid-jms-client + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/DeviceApplication.java b/storm-device/src/main/java/com/storm/device/DeviceApplication.java new file mode 100644 index 0000000..8346e33 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/DeviceApplication.java @@ -0,0 +1,48 @@ +package com.storm.device; + +import com.storm.common.security.annotation.EnableRyFeignClients; +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.env.Environment; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 认证授权中心 + * + * @author DengAo + */ +@Slf4j +@MapperScan("com.storm.device.mapper") +@EnableRyFeignClients +@SpringBootApplication +public class DeviceApplication +{ + public static void main(String[] args) throws UnknownHostException { + SpringApplication app = new SpringApplicationBuilder(DeviceApplication.class).build(args); + Environment env = app.run(args).getEnvironment(); + String protocol = "http"; + if (env.getProperty("server.ssl.key-store") != null) { + protocol = "https"; + } + log.info("--/\n---------------------------------------------------------------------------------------\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\t{}://localhost:{}\n\t" + + "External: \t{}://{}:{}\n\t" + + "Profile(s): \t{}" + + "\n---------------------------------------------------------------------------------------", + env.getProperty("spring.application.name"), + protocol, + env.getProperty("server.port"), + protocol, + InetAddress.getLocalHost().getHostAddress(), + env.getProperty("server.port"), + env.getActiveProfiles()); + log.info("--/\n------------------------------------------------------------------------------\n\t" ); + } +} diff --git a/storm-device/src/main/java/com/storm/device/config/IotClientConfig.java b/storm-device/src/main/java/com/storm/device/config/IotClientConfig.java new file mode 100644 index 0000000..63276f4 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/config/IotClientConfig.java @@ -0,0 +1,37 @@ +package com.storm.device.config; + +import com.huaweicloud.sdk.core.auth.BasicCredentials; +import com.huaweicloud.sdk.core.auth.ICredential; +import com.huaweicloud.sdk.core.region.Region; +import com.huaweicloud.sdk.iotda.v5.IoTDAClient; +import com.storm.device.config.properties.HuaWeiIotConfigProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +public class IotClientConfig { + + @Autowired + private HuaWeiIotConfigProperties huaWeiIotConfigProperties; + + @Bean + public IoTDAClient huaWeiIotInstance() { + log.info("RegionId:{},Endpoint:{}", huaWeiIotConfigProperties.getRegionId(), huaWeiIotConfigProperties.getEndpoint()); + ICredential auth = new BasicCredentials() + .withAk(huaWeiIotConfigProperties.getAk()) + .withSk(huaWeiIotConfigProperties.getSk()) + // 标准版/企业版需要使用衍生算法,基础版请删除配置"withDerivedPredicate" + .withDerivedPredicate(BasicCredentials.DEFAULT_DERIVED_PREDICATE) + .withProjectId(huaWeiIotConfigProperties.getProjectId()); + + return IoTDAClient.newBuilder() + .withCredential(auth) + // 标准版/企业版:需自行创建Region对象,基础版:请使用IoTDARegion的region对象,如"withRegion(IoTDARegion.CN_NORTH_4)" + .withRegion(new Region(huaWeiIotConfigProperties.getRegionId(), huaWeiIotConfigProperties.getEndpoint())) + // .withRegion(IoTDARegion.CN_NORTH_4) + .build(); + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/config/properties/HuaWeiIotConfigProperties.java b/storm-device/src/main/java/com/storm/device/config/properties/HuaWeiIotConfigProperties.java new file mode 100644 index 0000000..5ded5b8 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/config/properties/HuaWeiIotConfigProperties.java @@ -0,0 +1,146 @@ +package com.storm.device.config.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +/** + * @author DengAo + */ + +@Data +@NoArgsConstructor +@Configuration +@ConfigurationProperties(prefix = "huaweicloud") +public class HuaWeiIotConfigProperties { + + /** + * 访问Key + */ + private String ak; + + /** + * 访问秘钥 + */ + private String sk; + + /** + * 区域id + */ + private String regionId; + + /** + * 应用侧https接入地址 + */ + private String endpoint; + + /** + * 项目id + */ + private String projectId; + + /** + * 应用侧amqp接入地址 + */ + private String host; + + /** + * amqp连接端口 + */ + private int port = 5671; + + /** + * amqp接入凭证键值 + */ + private String accessKey; + + /** + * amqp接入凭证密钥 + */ + private String accessCode; + + // 指定单个进程启动的连接数 + // 单个连接消费速率有限,请参考使用限制,最大64个连接 + // 连接数和消费速率及rebalance相关,建议每500QPS增加一个连接 + //可根据实际情况自由调节,目前测试和正式环境资源有限,限制更改为4 + private int connectionCount = 4; + + /** + * 队列名称 + */ + private String queueName; + + /** + * 开门命令所属服务id + */ + private String smartDoorServiceId; + + /** + * 开门记录属性 + */ + private String doorOpenPropertyName; + + /** + * 开门命令 + */ + private String doorOpenCommandName; + + /** + * 设置临时密码命令 + */ + private String passwordSetCommandName; + + /** + * 仅支持true + */ + private boolean useSsl = true; + + /** + * IoTDA仅支持default + */ + private String vhost = "default"; + + /** + * IoTDA仅支持PLAIN + */ + private String saslMechanisms = "PLAIN"; + + /** + * true: SDK自动ACK(默认) + * false:收到消息后,需要手动调用message.acknowledge() + */ + private boolean isAutoAcknowledge = true; + + /** + * 重连时延(ms) + */ + private long reconnectDelay = 3000L; + + /** + * 最大重连时延(ms),随着重连次数增加重连时延逐渐增加 + */ + private long maxReconnectDelay = 30 * 1000L; + + /** + * 最大重连次数,默认值-1,代表没有限制 + */ + private long maxReconnectAttempts = -1; + + /** + * 空闲超时,对端在这个时间段内没有发送AMQP帧则会导致连接断开。默认值为30000。单位:毫秒。 + */ + private long idleTimeout = 30 * 1000L; + + /** + * The values below control how many messages the remote peer can send to the client and be held in a pre-fetch buffer for each consumer instance. + */ + private int queuePrefetch = 1000; + + /** + * 扩展参数 + */ + private Map extendedOptions; +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/controller/DeviceController.java b/storm-device/src/main/java/com/storm/device/controller/DeviceController.java new file mode 100644 index 0000000..16958f6 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/controller/DeviceController.java @@ -0,0 +1,296 @@ +package com.storm.device.controller; + +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.device.domain.po.Device; +import com.storm.device.domain.vo.DevicePointVO; +import com.storm.device.domain.vo.DeviceTotalVO; +import com.storm.device.domain.vo.DeviceVo; +import com.storm.device.service.IDeviceService; +import com.storm.device.service.IDeviceShadowService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/device") +@Tag(name = "设备基本信息相关接口") +public class DeviceController extends BaseController { + + @Autowired + private IDeviceService deviceService; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IDeviceShadowService deviceShadowService; + + /** + * 从物联网平台同步设备列表 + */ + @Operation(summary = "同步设备列表") + @PostMapping("/syncDeviceList") + public AjaxResult syncDeviceList() { + try { + deviceService.syncDeviceList(); + return success("设备列表同步成功"); + } catch (Exception e) { + log.error("同步设备列表失败", e); + return error("同步设备列表失败: " + e.getMessage()); + } + } + + /** + * 获取设备详细信息 + */ + @GetMapping(value = "/{deviceId}") + @Operation(summary = "获取设备详细信息") + public AjaxResult getInfo(@Parameter(description = "设备ID", required = true) + @PathVariable("deviceId") String deviceId) { + try { + DeviceVo deviceVo = deviceService.getInfo(deviceId); + if (deviceVo == null) { + return error("设备不存在"); + } + return success(deviceVo); + } catch (Exception e) { + log.error("获取设备信息失败: {}", deviceId, e); + return error("获取设备信息失败"); + } + } + + /** + * 查询设备上报数据 + */ + @Operation(summary = "查询设备上报数据") + @GetMapping("/queryServiceProperties/{deviceId}") + public AjaxResult queryServiceProperties(@PathVariable("deviceId") String deviceId) { + try { + List> list = deviceService.queryServiceProperties(deviceId); + return success(list); + } catch (Exception e) { + log.error("查询设备上报数据失败: {}", deviceId, e); + return error("查询设备上报数据失败"); + } + } + + /** + * 删除设备基本信息 + */ + @DeleteMapping("/{ids}") + @Operation(summary = "删除设备基本信息") + public AjaxResult remove(@Parameter(description = "设备基本信息ID数组", required = true) + @PathVariable Long[] ids) { + try { + return toAjax(deviceService.deleteDeviceByIds(ids)); + } catch (Exception e) { + log.error("删除设备失败", e); + return error("删除设备失败"); + } + } + + /** + * 导出设备基本信息列表 + */ + @PostMapping("/export") + @Operation(summary = "导出设备基本信息列表") + public void export(HttpServletResponse response, Device device) { + try { + List list = deviceService.exportDeviceList(device); + // Excel导出逻辑(根据实际框架调整) + // ExcelUtil util = new ExcelUtil(Device.class); + // util.exportExcel(response, list, "设备基本信息数据"); + } catch (Exception e) { + log.error("导出设备列表失败", e); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * 查询设备总数及在线离线设备数量 + */ + @Operation(summary = "查询设备总数及在线离线设备数量") + @GetMapping("/getTotal") + public AjaxResult getTotal() { + try { + DeviceTotalVO total = deviceService.getTotal(); + return success(total); + } catch (Exception e) { + log.error("查询设备总数失败", e); + return error("查询设备总数失败"); + } + } + + /** + * 获取设备坐标点数据 + */ + @Operation(summary = "获取设备坐标点数据") + @GetMapping("/getDeviceCoordinate") + public AjaxResult getDeviceCoordinate() { + try { + List coordinates = deviceService.getDeviceCoordinate(); + return success(coordinates); + } catch (Exception e) { + log.error("获取设备坐标失败", e); + return error("获取设备坐标失败"); + } + } + + /** + * 查询设备列表 + */ + @GetMapping("/list") + @Operation(summary = "查询设备列表") + public TableDataInfo list(Device device) { + try { + startPage(); + List list = deviceService.selectDeviceList(device); + return getDataTable(list); + } catch (Exception e) { + log.error("查询设备列表失败", e); + return getDataTable(null); + } + } + + /** + * 新增设备 + */ + @PostMapping + @Operation(summary = "新增设备") + public AjaxResult add(@RequestBody Device device) { + try { + return toAjax(deviceService.save(device)); + } catch (Exception e) { + log.error("新增设备失败", e); + return error("新增设备失败"); + } + } + + /** + * 修改设备 + */ + @PutMapping + @Operation(summary = "修改设备") + public AjaxResult edit(@RequestBody Device device) { + try { + return toAjax(deviceService.updateById(device)); + } catch (Exception e) { + log.error("修改设备失败", e); + return error("修改设备失败"); + } + } + + /** + * 获取设备运行统计 + */ + @GetMapping("/{deviceId}/runtime-stats") + @Operation(summary = "获取设备运行统计") + public AjaxResult getRuntimeStats(@PathVariable String deviceId) { + try { + Map stats = deviceShadowService.getRuntimeStats(deviceId); + return success(stats); + } catch (Exception e) { + log.error("获取设备运行统计失败: {}", deviceId, e); + return error("获取运行统计失败"); + } + } + + /** + * 获取设备按摩统计 + */ + @GetMapping("/{deviceId}/massage-stats") + @Operation(summary = "获取设备按摩统计") + public AjaxResult getMassageStats(@PathVariable String deviceId, + @RequestParam(defaultValue = "month") String timeRange) { + try { + Map stats = deviceShadowService.getMassageStats(deviceId, timeRange); + return success(stats); + } catch (Exception e) { + log.error("获取设备按摩统计失败: {}", deviceId, e); + return error("获取按摩统计失败"); + } + } + + /** + * 获取设备实时状态 + */ + @GetMapping("/{deviceId}/realtime-status") + @Operation(summary = "获取设备实时状态") + public AjaxResult getRealtimeStatus(@PathVariable String deviceId) { + try { + Map status = deviceShadowService.getRealtimeStatus(deviceId); + return success(status); + } catch (Exception e) { + log.error("获取设备实时状态失败: {}", deviceId, e); + return error("获取实时状态失败"); + } + } + + /** + * 获取设备状态历史 + */ + @GetMapping("/{deviceId}/status-history") + @Operation(summary = "获取设备状态历史") + public AjaxResult getStatusHistory(@PathVariable String deviceId, + @RequestParam(defaultValue = "50") Integer limit) { + try { + List> history = deviceShadowService.getStatusHistory(deviceId, limit); + return success(history); + } catch (Exception e) { + log.error("获取设备状态历史失败: {}", deviceId, e); + return error("获取状态历史失败"); + } + } + + /** + * 获取设备按摩历史 + */ + @GetMapping("/{deviceId}/massage-history") + @Operation(summary = "获取设备按摩历史") + public AjaxResult getMassageHistory(@PathVariable String deviceId, + @RequestParam(defaultValue = "50") Integer limit) { + try { + List> history = deviceShadowService.getMassageHistory(deviceId, limit); + return success(history); + } catch (Exception e) { + log.error("获取设备按摩历史失败: {}", deviceId, e); + return error("获取按摩历史失败"); + } + } + + /** + * 强制刷新设备状态 + */ + @PostMapping("/{deviceId}/refresh-status") + @Operation(summary = "强制刷新设备状态") + public AjaxResult refreshDeviceStatus(@PathVariable String deviceId) { + try { + // 从Redis强制刷新状态 + String statusKey = "device:status:" + deviceId; + String lastOnlineKey = "device:last_online:" + deviceId; + + Map result = new HashMap<>(); + result.put("status", redisTemplate.opsForValue().get(statusKey)); + result.put("lastOnlineTime", redisTemplate.opsForValue().get(lastOnlineKey)); + result.put("refreshTime", LocalDateTime.now().toString()); + + return success(result); + } catch (Exception e) { + log.error("刷新设备状态失败: {}", deviceId, e); + return error("刷新设备状态失败"); + } + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/controller/DeviceDataController.java b/storm-device/src/main/java/com/storm/device/controller/DeviceDataController.java new file mode 100644 index 0000000..121d14f --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/controller/DeviceDataController.java @@ -0,0 +1,31 @@ +package com.storm.device.controller; + +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.device.service.IDeviceStatusLogService; +import com.storm.device.service.IMassageTaskService; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * @author DengAo + * @date 2025/8/29 15:21 + */ +@RestController +@RequestMapping("/data") +@Tag(name = "设备数据相关接口") +public class DeviceDataController extends BaseController { + + @Resource + private IDeviceStatusLogService deviceStatusLogService; + + @Resource + private IMassageTaskService massageTaskService; + + + +} diff --git a/storm-device/src/main/java/com/storm/device/controller/DeviceHeadUsageController.java b/storm-device/src/main/java/com/storm/device/controller/DeviceHeadUsageController.java new file mode 100644 index 0000000..619e331 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/controller/DeviceHeadUsageController.java @@ -0,0 +1,117 @@ +package com.storm.device.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.storm.common.core.domain.R; +import com.storm.device.domain.po.DeviceHeadUsage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.device.service.IDeviceHeadUsageService; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.page.TableDataInfo; + +/** + * 设备按头使用统计Controller + * + * @author storm + * @date 2025-09-25 + */ +@RestController +@RequestMapping("/usage") +@Tag(name = "设备按头使用统计相关接口") +public class DeviceHeadUsageController extends BaseController +{ + @Autowired + private IDeviceHeadUsageService deviceHeadUsageService; + + /** + * 查询设备按头使用统计列表 + */ + @RequiresPermissions("device:usage:list") + @GetMapping("/list") + @Operation(summary = "查询设备按头使用统计列表") + public TableDataInfo> list(@Parameter(description = "设备按头使用统计查询条件") DeviceHeadUsage deviceHeadUsage) + { + startPage(); + List list = deviceHeadUsageService.selectDeviceHeadUsageList(deviceHeadUsage); + return getDataTable(list); + } + + /** + * 导出设备按头使用统计列表 + */ + @RequiresPermissions("device:usage:export") + @Log(title = "设备按头使用统计", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @Operation(summary = "导出设备按头使用统计列表") + public void export(HttpServletResponse response, @Parameter(description = "设备按头使用统计查询条件") DeviceHeadUsage deviceHeadUsage) + { + List list = deviceHeadUsageService.selectDeviceHeadUsageList(deviceHeadUsage); + ExcelUtil util = new ExcelUtil(DeviceHeadUsage.class); + util.exportExcel(response, list, "设备按头使用统计数据"); + } + + /** + * 获取设备按头使用统计详细信息 + */ + @RequiresPermissions("device:usage:query") + @GetMapping(value = "/{id}") + @Operation(summary = "获取设备按头使用统计详细信息") + public R getInfo(@Parameter(description = "设备按头使用统计ID", required = true) + @PathVariable("id") Long id) + { + return R.ok(deviceHeadUsageService.selectDeviceHeadUsageById(id)); + } + + /** + * 新增设备按头使用统计 + */ + @RequiresPermissions("device:usage:add") + @Log(title = "设备按头使用统计", businessType = BusinessType.INSERT) + @PostMapping + @Operation(summary = "新增设备按头使用统计") + public AjaxResult add(@Parameter(description = "设备按头使用统计实体", required = true) @RequestBody DeviceHeadUsage deviceHeadUsage) + { + return toAjax(deviceHeadUsageService.insertDeviceHeadUsage(deviceHeadUsage)); + } + + /** + * 修改设备按头使用统计 + */ + @RequiresPermissions("device:usage:edit") + @Log(title = "设备按头使用统计", businessType = BusinessType.UPDATE) + @PutMapping + @Operation(summary = "修改设备按头使用统计") + public AjaxResult edit(@Parameter(description = "设备按头使用统计实体", required = true) @RequestBody DeviceHeadUsage deviceHeadUsage) + { + return toAjax(deviceHeadUsageService.updateDeviceHeadUsage(deviceHeadUsage)); + } + + /** + * 删除设备按头使用统计 + */ + @RequiresPermissions("device:usage:remove") + @Log(title = "设备按头使用统计", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + @Operation(summary = "删除设备按头使用统计") + public AjaxResult remove(@Parameter(description = "设备按头使用统计ID数组", required = true) @PathVariable Long[] ids) + { + return toAjax(deviceHeadUsageService.deleteDeviceHeadUsageByIds(ids)); + } +} diff --git a/storm-device/src/main/java/com/storm/device/controller/DeviceRuntimeStatsController.java b/storm-device/src/main/java/com/storm/device/controller/DeviceRuntimeStatsController.java new file mode 100644 index 0000000..5443eed --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/controller/DeviceRuntimeStatsController.java @@ -0,0 +1,117 @@ +package com.storm.device.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.storm.common.core.domain.R; +import com.storm.device.domain.po.DeviceRuntimeStats; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.device.service.IDeviceRuntimeStatsService; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.page.TableDataInfo; + +/** + * 设备运行时长统计Controller + * + * @author storm + * @date 2025-09-25 + */ +@RestController +@RequestMapping("/stats") +@Tag(name = "设备运行时长统计相关接口") +public class DeviceRuntimeStatsController extends BaseController +{ + @Autowired + private IDeviceRuntimeStatsService deviceRuntimeStatsService; + + /** + * 查询设备运行时长统计列表 + */ + @RequiresPermissions("device:stats:list") + @GetMapping("/list") + @Operation(summary = "查询设备运行时长统计列表") + public TableDataInfo> list(@Parameter(description = "设备运行时长统计查询条件") DeviceRuntimeStats deviceRuntimeStats) + { + startPage(); + List list = deviceRuntimeStatsService.selectDeviceRuntimeStatsList(deviceRuntimeStats); + return getDataTable(list); + } + + /** + * 导出设备运行时长统计列表 + */ + @RequiresPermissions("device:stats:export") + @Log(title = "设备运行时长统计", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @Operation(summary = "导出设备运行时长统计列表") + public void export(HttpServletResponse response, @Parameter(description = "设备运行时长统计查询条件") DeviceRuntimeStats deviceRuntimeStats) + { + List list = deviceRuntimeStatsService.selectDeviceRuntimeStatsList(deviceRuntimeStats); + ExcelUtil util = new ExcelUtil(DeviceRuntimeStats.class); + util.exportExcel(response, list, "设备运行时长统计数据"); + } + + /** + * 获取设备运行时长统计详细信息 + */ + @RequiresPermissions("device:stats:query") + @GetMapping(value = "/{id}") + @Operation(summary = "获取设备运行时长统计详细信息") + public R getInfo(@Parameter(description = "设备运行时长统计ID", required = true) + @PathVariable("id") Long id) + { + return R.ok(deviceRuntimeStatsService.selectDeviceRuntimeStatsById(id)); + } + + /** + * 新增设备运行时长统计 + */ + @RequiresPermissions("device:stats:add") + @Log(title = "设备运行时长统计", businessType = BusinessType.INSERT) + @PostMapping + @Operation(summary = "新增设备运行时长统计") + public AjaxResult add(@Parameter(description = "设备运行时长统计实体", required = true) @RequestBody DeviceRuntimeStats deviceRuntimeStats) + { + return toAjax(deviceRuntimeStatsService.insertDeviceRuntimeStats(deviceRuntimeStats)); + } + + /** + * 修改设备运行时长统计 + */ + @RequiresPermissions("device:stats:edit") + @Log(title = "设备运行时长统计", businessType = BusinessType.UPDATE) + @PutMapping + @Operation(summary = "修改设备运行时长统计") + public AjaxResult edit(@Parameter(description = "设备运行时长统计实体", required = true) @RequestBody DeviceRuntimeStats deviceRuntimeStats) + { + return toAjax(deviceRuntimeStatsService.updateDeviceRuntimeStats(deviceRuntimeStats)); + } + + /** + * 删除设备运行时长统计 + */ + @RequiresPermissions("device:stats:remove") + @Log(title = "设备运行时长统计", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + @Operation(summary = "删除设备运行时长统计") + public AjaxResult remove(@Parameter(description = "设备运行时长统计ID数组", required = true) @PathVariable Long[] ids) + { + return toAjax(deviceRuntimeStatsService.deleteDeviceRuntimeStatsByIds(ids)); + } +} diff --git a/storm-device/src/main/java/com/storm/device/controller/DeviceShadowController.java b/storm-device/src/main/java/com/storm/device/controller/DeviceShadowController.java new file mode 100644 index 0000000..ac5cbd4 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/controller/DeviceShadowController.java @@ -0,0 +1,117 @@ +package com.storm.device.controller; + +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.device.service.IDeviceShadowService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/device/shadow") +public class DeviceShadowController { + + @Autowired + private IDeviceShadowService deviceShadowService; + + /** + * 获取设备影子数据 + */ + @GetMapping("/{deviceId}") + public AjaxResult getDeviceShadow(@PathVariable String deviceId) { + try { + Map shadowData = deviceShadowService.getDeviceShadowData(deviceId); + return AjaxResult.success(shadowData); + } catch (Exception e) { + log.error("获取设备影子失败: {}", deviceId, e); + return AjaxResult.error("获取设备影子数据失败"); + } + } + + /** + * 获取设备运行统计 + */ + @GetMapping("/{deviceId}/runtime-stats") + public AjaxResult getRuntimeStats(@PathVariable String deviceId) { + try { + Map stats = deviceShadowService.getRuntimeStats(deviceId); + return AjaxResult.success(stats); + } catch (Exception e) { + log.error("获取运行统计失败: {}", deviceId, e); + return AjaxResult.error("获取运行统计失败"); + } + } + + /** + * 获取设备按摩统计 + */ + @GetMapping("/{deviceId}/massage-stats") + public AjaxResult getMassageStats(@PathVariable String deviceId, + @RequestParam(defaultValue = "month") String timeRange) { + try { + Map stats = deviceShadowService.getMassageStats(deviceId, timeRange); + return AjaxResult.success(stats); + } catch (Exception e) { + log.error("获取按摩统计失败: {}", deviceId, e); + return AjaxResult.error("获取按摩统计失败"); + } + } + + /** + * 获取设备实时状态 + */ + @GetMapping("/{deviceId}/realtime-status") + public AjaxResult getRealtimeStatus(@PathVariable String deviceId) { + try { + Map status = deviceShadowService.getRealtimeStatus(deviceId); + return AjaxResult.success(status); + } catch (Exception e) { + log.error("获取实时状态失败: {}", deviceId, e); + return AjaxResult.error("获取实时状态失败"); + } + } + + /** + * 获取设备状态历史 + */ + @GetMapping("/{deviceId}/status-history") + public AjaxResult getStatusHistory(@PathVariable String deviceId, + @RequestParam(defaultValue = "50") Integer limit) { + try { + return AjaxResult.success(deviceShadowService.getStatusHistory(deviceId, limit)); + } catch (Exception e) { + log.error("获取状态历史失败: {}", deviceId, e); + return AjaxResult.error("获取状态历史失败"); + } + } + + /** + * 获取设备按摩历史 + */ + @GetMapping("/{deviceId}/massage-history") + public AjaxResult getMassageHistory(@PathVariable String deviceId, + @RequestParam(defaultValue = "50") Integer limit) { + try { + return AjaxResult.success(deviceShadowService.getMassageHistory(deviceId, limit)); + } catch (Exception e) { + log.error("获取按摩历史失败: {}", deviceId, e); + return AjaxResult.error("获取按摩历史失败"); + } + } + + /** + * 获取设备健康状态 + */ + @GetMapping("/{deviceId}/health-status") + public AjaxResult getHealthStatus(@PathVariable String deviceId) { + try { + Map healthStatus = deviceShadowService.getHealthStatus(deviceId); + return AjaxResult.success(healthStatus); + } catch (Exception e) { + log.error("获取设备健康状态失败: {}", deviceId, e); + return AjaxResult.error("获取健康状态失败"); + } + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/controller/DeviceStatusLogController.java b/storm-device/src/main/java/com/storm/device/controller/DeviceStatusLogController.java new file mode 100644 index 0000000..085bac7 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/controller/DeviceStatusLogController.java @@ -0,0 +1,120 @@ +package com.storm.device.controller; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import com.storm.common.core.domain.R; +import com.storm.device.domain.po.DeviceStatusLog; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.device.service.IDeviceStatusLogService; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.page.TableDataInfo; + +/** + * 设备状态日志Controller + * + * @author storm + * @date 2025-09-24 + */ +@RestController +@RequestMapping("/log") +@Tag(name = "设备状态日志相关接口") +public class DeviceStatusLogController extends BaseController +{ + @Autowired + private IDeviceStatusLogService deviceStatusLogService; + + /** + * 查询设备状态日志列表 + */ + @RequiresPermissions("device:device:list") + @GetMapping("/list") + @Operation(summary = "查询设备状态日志列表") + public TableDataInfo> list(@Parameter(description = "设备状态日志查询条件") DeviceStatusLog deviceStatusLog) + { + startPage(); + System.out.println("Sdasdas"); + TableDataInfo dataTable = getDataTable(deviceStatusLogService.selectDeviceStatusLogList(deviceStatusLog)); + System.out.println(dataTable); + return dataTable; + } + + /** + * 导出设备状态日志列表 + */ + @RequiresPermissions("device:log:export") + @Log(title = "设备状态日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @Operation(summary = "导出设备状态日志列表") + public void export(HttpServletResponse response, @Parameter(description = "设备状态日志查询条件") DeviceStatusLog deviceStatusLog) + { + List list = deviceStatusLogService.selectDeviceStatusLogList(deviceStatusLog); + ExcelUtil util = new ExcelUtil(DeviceStatusLog.class); + util.exportExcel(response, list, "设备状态日志数据"); + } + + /** + * 获取设备状态日志详细信息 + */ + @RequiresPermissions("device:log:query") + @GetMapping(value = "/{id}") + @Operation(summary = "获取设备状态日志详细信息") + public R getInfo(@Parameter(description = "设备状态日志ID", required = true) + @PathVariable("id") Long id) + { + return R.ok(deviceStatusLogService.getById(id)); + } + + /** + * 新增设备状态日志 + */ + @RequiresPermissions("device:log:add") + @Log(title = "设备状态日志", businessType = BusinessType.INSERT) + @PostMapping + @Operation(summary = "新增设备状态日志") + public AjaxResult add(@Parameter(description = "设备状态日志实体", required = true) @RequestBody DeviceStatusLog deviceStatusLog) + { + return toAjax(deviceStatusLogService.save(deviceStatusLog)); + } + + /** + * 修改设备状态日志 + */ + @RequiresPermissions("device:log:edit") + @Log(title = "设备状态日志", businessType = BusinessType.UPDATE) + @PutMapping + @Operation(summary = "修改设备状态日志") + public AjaxResult edit(@Parameter(description = "设备状态日志实体", required = true) @RequestBody DeviceStatusLog deviceStatusLog) + { + return toAjax(deviceStatusLogService.updateById(deviceStatusLog)); + } + + /** + * 删除设备状态日志 + */ + @RequiresPermissions("device:log:remove") + @Log(title = "设备状态日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + @Operation(summary = "删除设备状态日志") + public AjaxResult remove(@Parameter(description = "设备状态日志ID数组", required = true) @PathVariable Long[] ids) + { + return toAjax(deviceStatusLogService.removeByIds(Arrays.asList(ids))); + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/controller/MassageTaskController.java b/storm-device/src/main/java/com/storm/device/controller/MassageTaskController.java new file mode 100644 index 0000000..f9c0899 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/controller/MassageTaskController.java @@ -0,0 +1,235 @@ +package com.storm.device.controller; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.*; +import javax.servlet.http.HttpServletResponse; +import com.storm.common.core.domain.R; +import com.storm.common.core.utils.StringUtils; +import com.storm.device.domain.po.MassageTask; +import com.storm.device.service.IMassageTaskService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.page.TableDataInfo; + +/** + * 按摩任务记录Controller + * + * @author storm + * @date 2025-08-28 + */ +@RestController +@RequestMapping("/massage") +@Tag(name = "按摩任务记录相关接口") +public class MassageTaskController extends BaseController { + @Autowired + private IMassageTaskService massageTaskService; + + // 查询按摩头类型统计数据 + @Operation(summary = "查询按摩头类型统计数据") + @GetMapping("/headTypeStats") + public AjaxResult getHeadTypeStats(@RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime) { + List> data = massageTaskService.getHeadTypeStats(startTime, endTime); + return success(data); + } + + // 查询按摩部位统计数据 + @Operation(summary = "查询按摩部位统计数据") + @GetMapping("/bodyPartStats") + public AjaxResult getBodyPartStats(@RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime) { + List> data = massageTaskService.getBodyPartStats(startTime, endTime); + return success(data); + } + + // 热门使用时间热力图统计 + @Operation(summary = "热门使用时间热力图统计") + @GetMapping("/getHeatMap") + public AjaxResult getMassageTaskStatsByRange(@RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime) { + return success(massageTaskService.getMassageTaskStatsByRange(startTime, endTime)); + } + + // 统计设备总按摩时间 + @Operation(summary = "统计设备总按摩时间") + @GetMapping("/getDeviceTotalMassageTime") + public AjaxResult getDeviceMassageStats( + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime) { + List> stats = massageTaskService.getDeviceMassageStats(startTime, endTime); + return AjaxResult.success(stats); + } + + /** + * 按摩总时长趋势 + * + * @param startTime + * @param endTime + * @return + */ + @Operation(summary = "按摩总时长趋势") + @GetMapping("/getDailyStats") + public AjaxResult getDailyStats( + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime) { + + try { + // 如果只传一个,补另一个 + LocalDate start; + LocalDate end; + + if (StringUtils.hasText(startTime)) { + start = LocalDate.parse(startTime); + } else { + // 不传 startTime,查表里最早的时间 + start = massageTaskService.getMinDate(); + + if (start == null) { + // 没数据,直接返回空数组 + return AjaxResult.success(Collections.emptyList()); + } + } + + if (StringUtils.hasText(endTime)) { + end = LocalDate.parse(endTime); + } else { + // 不传 endTime,默认查到今天 + end = LocalDate.now(); + } + + if (start.isAfter(end)) { + return AjaxResult.error(400, "startTime 不能晚于 endTime"); + } + + System.out.println("---------------------------------------------------------------------------"); + List> stats = massageTaskService.getDailyStats(start.toString(), end.toString()); + return AjaxResult.success(stats); + + } catch (DateTimeParseException e) { + return AjaxResult.error(400, "时间格式不正确,应为 YYYY-MM-DD"); + } catch (Exception e) { + return AjaxResult.error(500, "查询出错:" + e.getMessage()); + } + } + + /** + * 分析按摩总时长的环比增长趋势 + * + * @param startTime + * @param endTime + * @return + */ + @Operation(summary = "分析按摩总时长的环比增长趋势") + @GetMapping("/getTrendInsight") + public AjaxResult getTrendInsight(@RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime) { + return massageTaskService.getTrendInsight(startTime, endTime); + } + + /** + * 按摩任务的综合统计指标 + * + * @return + */ + @Operation(summary = "按摩任务的综合统计指标") + @GetMapping("/stats") + public AjaxResult getStats() { + Map stats = massageTaskService.getStats(); + return success(stats); + } + + /** + * 按摩任务的总计数据 + * + * @return + */ + @Operation(summary = "按摩任务的总计数据") + @GetMapping("/total") + public AjaxResult getTotals() { + + Map data = massageTaskService.getTotals(); + return success(data); + + } + + /** + * 查询按摩任务列表 + */ + @RequiresPermissions("device:task:list") + @GetMapping("/list") + @Operation(summary = "查询按摩任务列表") + public TableDataInfo> list(@Parameter(description = "按摩任务查询条件") MassageTask massageTask) + { + startPage(); + return getDataTable(massageTaskService.selectMassageTaskList(massageTask)); + } + + /** + * 导出按摩任务列表 + */ + @RequiresPermissions("device:task:export") + @Log(title = "按摩任务", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @Operation(summary = "导出按摩任务列表") + public void export(HttpServletResponse response, @Parameter(description = "按摩任务查询条件") MassageTask massageTask) + { + List list = massageTaskService.selectMassageTaskList(massageTask); + ExcelUtil util = new ExcelUtil(MassageTask.class); + util.exportExcel(response, list, "按摩任务数据"); + } + + /** + * 获取按摩任务详细信息 + */ + @RequiresPermissions("device:task:query") + @GetMapping(value = "/{id}") + @Operation(summary = "获取按摩任务详细信息") + public R getInfo(@Parameter(description = "按摩任务ID", required = true) + @PathVariable("id") Long id) + { + return R.ok(massageTaskService.getById(id)); + } + + /** + * 新增按摩任务 + */ + @RequiresPermissions("device:task:add") + @Log(title = "按摩任务", businessType = BusinessType.INSERT) + @PostMapping + @Operation(summary = "新增按摩任务") + public AjaxResult add(@Parameter(description = "按摩任务实体", required = true) @RequestBody MassageTask massageTask) + { + return toAjax(massageTaskService.save(massageTask)); + } + + /** + * 修改按摩任务 + */ + @RequiresPermissions("device:task:edit") + @Log(title = "按摩任务", businessType = BusinessType.UPDATE) + @PutMapping + @Operation(summary = "修改按摩任务") + public AjaxResult edit(@Parameter(description = "按摩任务实体", required = true) @RequestBody MassageTask massageTask) + { + return toAjax(massageTaskService.updateById(massageTask)); + } + + /** + * 删除按摩任务 + */ + @RequiresPermissions("device:task:remove") + @Log(title = "按摩任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + @Operation(summary = "删除按摩任务") + public AjaxResult remove(@Parameter(description = "按摩任务ID数组", required = true) @PathVariable Long[] ids) + { + return toAjax(massageTaskService.removeBatchByIds(Arrays.asList(ids))); + } +} diff --git a/storm-device/src/main/java/com/storm/device/domain/dto/MassageTaskStatDTO.java b/storm-device/src/main/java/com/storm/device/domain/dto/MassageTaskStatDTO.java new file mode 100644 index 0000000..6652039 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/dto/MassageTaskStatDTO.java @@ -0,0 +1,10 @@ +package com.storm.device.domain.dto; + +import lombok.Data; + +@Data +public class MassageTaskStatDTO { + private Integer startHour; + private Integer weekDay; + private Integer frequency; +} diff --git a/storm-device/src/main/java/com/storm/device/domain/dto/SyncDeviceDTO.java b/storm-device/src/main/java/com/storm/device/domain/dto/SyncDeviceDTO.java new file mode 100644 index 0000000..7d868f8 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/dto/SyncDeviceDTO.java @@ -0,0 +1,29 @@ +package com.storm.device.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author DengAo + * @date 2025/8/28 11:26 + */ +@Data +public class SyncDeviceDTO { + @Schema(description = "设备唯一标识") + private String deviceId; + + @Schema(description = "设备名称") + private String deviceName; + + @Schema(description = "产品ID") + private String productId; + + @Schema(description = "产品名称") + private String productName; + + @Schema(description = "设备类型(1:RS-LL-X1,2:RS-LL-X2)") + private Integer deviceType; + + @Schema(description = "是否删除") + private Integer isDeleted; +} diff --git a/storm-device/src/main/java/com/storm/device/domain/page/DevicePageQuery.java b/storm-device/src/main/java/com/storm/device/domain/page/DevicePageQuery.java new file mode 100644 index 0000000..86f2a6f --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/page/DevicePageQuery.java @@ -0,0 +1,41 @@ +package com.storm.device.domain.page; + +import com.storm.common.core.domain.query.PageQuery; +import com.storm.common.core.utils.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * @author DengAo + * @date 2025/8/28 16:42 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Schema(description = "设备分页查询条件") +public class DevicePageQuery extends PageQuery { + + @Schema(description = "设备名称参数") + private String deviceNameParam; + + @Schema(description = "设备在线状态参数") + private String statusParam; + + @Schema(description = "软件版本参数") + private String softwareVersionParam; + + @Schema(description = "参数服务器版本参数") + private String vtxdbVersionParam; + + @Schema(description = "最后在线时间区间的开始时间", example = "2022-7-18 19:52:36") + @DateTimeFormat(pattern = DateUtils.DEFAULT_DATE_TIME_FORMAT) + private Date beginTime; + + @Schema(description = "最后在线时间区间的结束时间", example = "2022-7-18 19:52:36") + @DateTimeFormat(pattern = DateUtils.DEFAULT_DATE_TIME_FORMAT) + private Date endTime; +} diff --git a/storm-device/src/main/java/com/storm/device/domain/po/Device.java b/storm-device/src/main/java/com/storm/device/domain/po/Device.java new file mode 100644 index 0000000..dc14b7a --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/po/Device.java @@ -0,0 +1,76 @@ +package com.storm.device.domain.po; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("device") +public class Device { + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String deviceId; + private String deviceName; + private String productId; + private String productName; + private Integer deviceType; + private String status; + + // 位置信息 + private String location; + private String shopId; + private String longitude; + private String latitude; + + // 版本信息 + private String softwareVersion; + private String vtxdbVersion; + + // 许可证信息 + private String activationCode; + private String serialNumber; + private String licenseStatus; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date licenseExpireTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date lastMassageTime; + + // 在线状态时间记录 + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date lastOnlineTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date lastOfflineTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date activationTime; + + // 运行时长统计(毫秒) + private Long totalOnlineDuration; // 累计在线时长(秒) + private Long dailyDuration; // 今日运行时长(秒) + private Long weeklyDuration; // 本周运行时长(秒) + private Long monthlyDuration; // 本月运行时长(秒) + + private String description; + private String remark; + + @TableLogic + private Integer isDeleted; + private String createBy; + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/domain/po/DeviceHeadUsage.java b/storm-device/src/main/java/com/storm/device/domain/po/DeviceHeadUsage.java new file mode 100644 index 0000000..d3ef285 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/po/DeviceHeadUsage.java @@ -0,0 +1,73 @@ +package com.storm.device.domain.po; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.web.domain.BaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * 设备按头使用统计对象 device_head_usage + * + * @author storm + * @date 2025-08-22 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@TableName("device_head_usage") +@Schema(description = "设备按头使用统计实体") +public class DeviceHeadUsage extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @Schema(description = "主键ID") + private Long id; + + /** 设备唯一标识 */ + @Excel(name = "设备唯一标识") + @Schema(description = "设备唯一标识") + private String deviceId; + + /** 按头类型 */ + @Excel(name = "按头类型") + @Schema(description = "按头类型") + private String headType; + + /** 使用次数 */ + @Excel(name = "使用次数") + @Schema(description = "使用次数") + private Integer usageCount; + + /** 总使用时长(秒) */ + @Excel(name = "总使用时长") + @Schema(description = "总使用时长(秒)") + private Long totalDuration; + + /** 今日使用时长(秒) */ + @Excel(name = "今日使用时长") + @Schema(description = "今日使用时长(秒)") + private Long dailyDuration; + + /** 本周使用时长(秒) */ + @Excel(name = "本周使用时长") + @Schema(description = "本周使用时长(秒)") + private Long weeklyDuration; + + /** 本月使用时长(秒) */ + @Excel(name = "本月使用时长") + @Schema(description = "本月使用时长(秒)") + private Long monthlyDuration; + + /** 最后使用时间 */ + @Excel(name = "最后使用时间") + @Schema(description = "最后使用时间") + private Date lastUsedTime; +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/domain/po/DeviceRuntimeStats.java b/storm-device/src/main/java/com/storm/device/domain/po/DeviceRuntimeStats.java new file mode 100644 index 0000000..79aa411 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/po/DeviceRuntimeStats.java @@ -0,0 +1,71 @@ +package com.storm.device.domain.po; + +import com.baomidou.mybatisplus.annotation.*; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.web.domain.BaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * 设备运行时长统计对象 device_runtime_stats + * + * @author storm + * @date 2025-08-22 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@TableName("device_runtime_stats") +@Schema(description = "设备运行时长统计实体") +public class DeviceRuntimeStats extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @Schema(description = "主键ID") + @TableId(type = IdType.AUTO) + private Long id; + + /** 设备唯一标识 */ + @Excel(name = "设备唯一标识") + @Schema(description = "设备唯一标识") + private String deviceId; + + /** 统计日期 */ + @Excel(name = "统计日期") + @Schema(description = "统计日期") + @TableField("stat_date") + private Date statDate; + + /** 日运行时长(秒) */ + @Excel(name = "日运行时长") + @Schema(description = "日运行时长(秒)") + private Long dailyDuration; + + /** 周运行时长(秒) */ + @Excel(name = "周运行时长") + @Schema(description = "周运行时长(秒)") + private Long weeklyDuration; + + /** 月运行时长(秒) */ + @Excel(name = "月运行时长") + @Schema(description = "月运行时长(秒)") + private Long monthlyDuration; + + /** 上线次数 */ + @Excel(name = "上线次数") + @Schema(description = "上线次数") + private Integer onlineCount; + + @TableField(fill = FieldFill.INSERT) + private Date createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/domain/po/DeviceStatusLog.java b/storm-device/src/main/java/com/storm/device/domain/po/DeviceStatusLog.java new file mode 100644 index 0000000..7ded343 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/po/DeviceStatusLog.java @@ -0,0 +1,65 @@ +package com.storm.device.domain.po; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("device_status_log") +public class DeviceStatusLog { + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String deviceId; + private String status; + private String eventType; + + // 毫秒时间戳 + private Long massageStartTime; + private Long massageEndTime; + private Long massageDuration; + + private String headType; + private String bodyPart; + private String massagePlan; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date eventTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date lastOnlineTime; + + private Integer duration; + + @TableLogic + private Integer isDeleted; + private String createBy; + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + // 转换方法:毫秒时间戳转Date + @TableField(exist = false) + private Date startTimeDate; + + @TableField(exist = false) + private Date endTimeDate; + + public Date getStartTimeDate() { + return massageStartTime != null ? new Date(massageStartTime) : null; + } + + public Date getEndTimeDate() { + return massageEndTime != null ? new Date(massageEndTime) : null; + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/domain/po/MassageTask.java b/storm-device/src/main/java/com/storm/device/domain/po/MassageTask.java new file mode 100644 index 0000000..c288cb7 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/po/MassageTask.java @@ -0,0 +1,58 @@ +package com.storm.device.domain.po; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("massage_task") +public class MassageTask { + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String deviceId; + private String deviceName; + private String productName; + private String headType; + private String bodyPart; + private String massagePlan; + + private String createBy; + + // 毫秒时间戳 + private Long startTime; + private Long endTime; + private Long taskTime; + private Long createTimestamp; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + // 转换方法 + @TableField(exist = false) + private Date startTimeDate; + + @TableField(exist = false) + private Date endTimeDate; + + public Date getStartTimeDate() { + return startTime != null ? new Date(startTime) : null; + } + + public Date getEndTimeDate() { + return endTime != null ? new Date(endTime) : null; + } + + // 计算任务时长 + public void calculateDuration() { + if (startTime != null && endTime != null && endTime > startTime) { + this.taskTime = endTime - startTime; + } + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/domain/vo/DevicePointVO.java b/storm-device/src/main/java/com/storm/device/domain/vo/DevicePointVO.java new file mode 100644 index 0000000..3ec9285 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/vo/DevicePointVO.java @@ -0,0 +1,17 @@ +package com.storm.device.domain.vo; + +import lombok.Data; + +/** + * 设备坐标点响应类 + * + * @author DengAo + * @date 2025/9/3 14:51 + */ +@Data +public class DevicePointVO { + private String name; + private Double lng; // 经度 + private Double lat; // 纬度 + +} diff --git a/storm-device/src/main/java/com/storm/device/domain/vo/DeviceTotalVO.java b/storm-device/src/main/java/com/storm/device/domain/vo/DeviceTotalVO.java new file mode 100644 index 0000000..400b27e --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/vo/DeviceTotalVO.java @@ -0,0 +1,20 @@ +package com.storm.device.domain.vo; + +import lombok.Data; +import java.util.List; + +/** + * @author DengAo + * @date 2025/9/3 12:45 + */ +@Data +public class DeviceTotalVO { + private String total; + private List data; + + @Data + public static class DeviceStatusData { + private Integer value; + private String name; + } +} diff --git a/storm-device/src/main/java/com/storm/device/domain/vo/DeviceVo.java b/storm-device/src/main/java/com/storm/device/domain/vo/DeviceVo.java new file mode 100644 index 0000000..a585304 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/vo/DeviceVo.java @@ -0,0 +1,74 @@ +package com.storm.device.domain.vo; + +import com.storm.common.core.annotation.Excel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import java.util.Date; + +/** + * @author DengAo + * @date 2025/8/28 16:04 + */ +@Data +@Schema(description = "设备信息响应模型") +public class DeviceVo { + /** 设备唯一标识 */ + @Excel(name = "设备唯一标识") + @Schema(description = "设备唯一标识") + private String deviceId; + + /** 设备名称 */ + @Excel(name = "设备名称") + @Schema(description = "设备名称") + private String deviceName; + + /** 产品ID */ + @Excel(name = "产品ID") + @Schema(description = "产品ID") + private String productId; + + /** 产品名称 */ + @Excel(name = "产品名称") + @Schema(description = "产品名称") + private String productName; + + /** 设备类型 */ + @Excel(name = "设备类型(1:RS-LL-X1,2:RS-LL-X2)") + @Schema(description = "设备类型(1:RS-LL-X1,2:RS-LL-X2)") + private Integer deviceType; + + /** 设备在线状态 */ + @Excel(name = "设备在线状态,ONLINE:设备在线,OFFLINE:设备离线") + @Schema(description = "设备在线状态,ONLINE:设备在线,OFFLINE:设备离线") + private String status; + + /** 最后在线时间 */ + @Excel(name = "最后在线时间") + @Schema(description = "最后在线时间") + private Date lastOnlineTime; + + /** 最后按摩时间 */ + @Schema(description = "最后按摩时间") + private Date lastMassageTime; + + /** 安装位置 */ + @Excel(name = "安装位置") + @Schema(description = "安装位置") + private String location; + + /** 经度 */ + @Schema(description = "经度") + private String longitude; + + /** 纬度 */ + @Schema(description = "纬度") + private String latitude; + + /** 软件版本 */ + @Schema(description = "软件版本") + private String softwareVersion; + + /** 参数服务器版本 */ + @Schema(description = "参数服务器版本") + private String vtxdbVersion; +} diff --git a/storm-device/src/main/java/com/storm/device/domain/vo/ProductVo.java b/storm-device/src/main/java/com/storm/device/domain/vo/ProductVo.java new file mode 100644 index 0000000..b51749a --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/domain/vo/ProductVo.java @@ -0,0 +1,25 @@ +package com.storm.device.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 产品信息响应模型 + * + * @author itcast + **/ +@Data +@Schema(description = "产品信息响应模型") +public class ProductVo { + /** + * 产品的ProductKey,物联网平台产品唯一标识 + */ + @Schema(description = "产品的ProductKey,物联网平台产品唯一标识") + private String productId; + + /** + * 产品名称 + */ + @Schema(description = "产品名称") + private String name; +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/handler/DuplicateDataHandler.java b/storm-device/src/main/java/com/storm/device/handler/DuplicateDataHandler.java new file mode 100644 index 0000000..306fcd0 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/handler/DuplicateDataHandler.java @@ -0,0 +1,28 @@ +package com.storm.device.handler; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Slf4j +public class DuplicateDataHandler { + + /** + * 处理重复数据异常 + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void handleDuplicateKeyException(Runnable operation, String operationName) { + try { + operation.run(); + } catch (DuplicateKeyException e) { + log.warn("重复数据已存在,跳过{}操作", operationName); + // 可以记录到日志或监控系统 + } catch (Exception e) { + log.error("{}操作失败", operationName, e); + throw e; + } + } +} diff --git a/storm-device/src/main/java/com/storm/device/manager/DataConsistencyService.java b/storm-device/src/main/java/com/storm/device/manager/DataConsistencyService.java new file mode 100644 index 0000000..db0993b --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/manager/DataConsistencyService.java @@ -0,0 +1,182 @@ +package com.storm.device.manager; + +import com.storm.device.mapper.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +public class DataConsistencyService { + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper; + + @Autowired + private MassageTaskMapper massageTaskMapper; + + @Autowired + private DeviceHeadUsageMapper deviceHeadUsageMapper; + + /** + * 检查设备数据一致性 + */ + public Map checkDeviceDataConsistency(String deviceId) { + Map result = new HashMap<>(); + + try { + // 1. 检查设备总时长与运行统计是否一致 + boolean totalDurationConsistent = checkTotalDurationConsistency(deviceId); + result.put("totalDurationConsistent", totalDurationConsistent); + + // 2. 检查按头使用统计与按摩任务是否一致 + boolean headUsageConsistent = checkHeadUsageConsistency(deviceId); + result.put("headUsageConsistent", headUsageConsistent); + + // 3. 检查运行统计与状态日志是否一致 + boolean runtimeStatsConsistent = checkRuntimeStatsConsistency(deviceId); + result.put("runtimeStatsConsistent", runtimeStatsConsistent); + + result.put("success", true); + result.put("message", "数据一致性检查完成"); + + } catch (Exception e) { + log.error("检查设备数据一致性失败,设备: {}", deviceId, e); + result.put("success", false); + result.put("message", "检查失败: " + e.getMessage()); + } + + return result; + } + + private boolean checkTotalDurationConsistency(String deviceId) { + try { + // 查询设备表中的总时长 + String deviceTotalSql = "SELECT total_online_duration FROM device WHERE device_id = '" + deviceId + "'"; + // 执行查询并获取结果 + + // 查询运行统计表中的总时长 + String statsTotalSql = "SELECT COALESCE(SUM(daily_duration), 0) as total_duration " + + "FROM device_runtime_stats WHERE device_id = '" + deviceId + "'"; + // 执行查询并获取结果 + + // 比较两个值是否一致(允许微小差异) + // Long deviceTotal = ...; + // Long statsTotal = ...; + // return Math.abs(deviceTotal - statsTotal) <= 60; // 允许60秒差异 + + return true; // 简化处理 + } catch (Exception e) { + log.error("检查总时长一致性失败", e); + return false; + } + } + + private boolean checkHeadUsageConsistency(String deviceId) { + try { + // 检查按头使用统计与按摩任务数据是否一致 + // 实现具体的检查逻辑 + return true; // 简化处理 + } catch (Exception e) { + log.error("检查按头使用一致性失败", e); + return false; + } + } + + private boolean checkRuntimeStatsConsistency(String deviceId) { + try { + // 检查运行统计与状态日志数据是否一致 + // 实现具体的检查逻辑 + return true; // 简化处理 + } catch (Exception e) { + log.error("检查运行统计一致性失败", e); + return false; + } + } + + /** + * 修复数据不一致问题 + */ + public Map fixDataInconsistency(String deviceId) { + Map result = new HashMap<>(); + + try { + log.info("开始修复设备数据不一致问题: {}", deviceId); + + // 1. 修复总时长不一致 + fixTotalDuration(deviceId); + + // 2. 修复按头使用统计不一致 + fixHeadUsageStatistics(deviceId); + + // 3. 修复运行统计不一致 + fixRuntimeStatistics(deviceId); + + result.put("success", true); + result.put("message", "数据修复完成"); + + } catch (Exception e) { + log.error("修复设备数据不一致失败,设备: {}", deviceId, e); + result.put("success", false); + result.put("message", "修复失败: " + e.getMessage()); + } + + return result; + } + + private void fixTotalDuration(String deviceId) { + try { + String fixSql = "UPDATE device d " + + "SET total_online_duration = (" + + " SELECT COALESCE(SUM(daily_duration), 0) " + + " FROM device_runtime_stats s " + + " WHERE s.device_id = d.device_id" + + "), " + + "update_time = NOW() " + + "WHERE d.device_id = '" + deviceId + "'"; + + deviceMapper.executeNativeSQL(fixSql); + log.info("修复设备总时长完成: {}", deviceId); + } catch (Exception e) { + log.error("修复设备总时长失败: {}", deviceId, e); + throw e; + } + } + + private void fixHeadUsageStatistics(String deviceId) { + try { + // 删除该设备的按头使用统计 + String deleteSql = "DELETE FROM device_head_usage WHERE device_id = '" + deviceId + "'"; + deviceHeadUsageMapper.executeNativeSQL(deleteSql); + + // 重新生成统计 + String insertSql = "INSERT INTO device_head_usage (device_id, head_type, usage_count, total_duration, " + + "daily_duration, weekly_duration, monthly_duration, last_used_time, create_time, update_time) " + + "SELECT device_id, head_type, COUNT(*) as usage_count, " + + "COALESCE(SUM(task_time), 0) as total_duration, " + + "COALESCE(SUM(CASE WHEN DATE(create_time) = CURDATE() THEN task_time ELSE 0 END), 0) as daily_duration, " + + "COALESCE(SUM(CASE WHEN create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN task_time ELSE 0 END), 0) as weekly_duration, " + + "COALESCE(SUM(CASE WHEN create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN task_time ELSE 0 END), 0) as monthly_duration, " + + "MAX(create_time) as last_used_time, NOW() as create_time, NOW() as update_time " + + "FROM massage_task " + + "WHERE device_id = '" + deviceId + "' " + + "GROUP BY device_id, head_type"; + + deviceHeadUsageMapper.executeNativeSQL(insertSql); + log.info("修复按头使用统计完成: {}", deviceId); + } catch (Exception e) { + log.error("修复按头使用统计失败: {}", deviceId, e); + throw e; + } + } + + private void fixRuntimeStatistics(String deviceId) { + // 实现运行统计修复逻辑 + log.info("修复运行统计: {}", deviceId); + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/manager/DeviceManagerService.java b/storm-device/src/main/java/com/storm/device/manager/DeviceManagerService.java new file mode 100644 index 0000000..9147a5a --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/manager/DeviceManagerService.java @@ -0,0 +1,229 @@ +package com.storm.device.manager; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.storm.device.domain.po.Device; +import com.storm.device.mapper.DeviceMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +@Service +@Slf4j +public class DeviceManagerService { + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private RedisTemplate redisTemplate; + + private static final String DEVICE_LOCK_PREFIX = "device:lock:"; + private static final long LOCK_EXPIRE_SECONDS = 10; + + /** + * 终极解决方案:使用数据库的唯一约束作为最终保障 + */ + public Device getOrCreateDevice(String originalDeviceId, Consumer initializer) { + String normalizedDeviceId = normalizeDeviceId(originalDeviceId); + + if (StrUtil.isBlank(normalizedDeviceId)) { + throw new IllegalArgumentException("设备ID不能为空"); + } + + // 第一步:快速查询(无锁) + Device existingDevice = deviceMapper.selectOne( + new LambdaQueryWrapper().eq(Device::getDeviceId, normalizedDeviceId)); + if (existingDevice != null) { + return existingDevice; + } + + // 第二步:使用更严格的锁机制 + String lockKey = DEVICE_LOCK_PREFIX + normalizedDeviceId; + boolean locked = false; + + try { + // 获取分布式锁 + locked = tryLock(lockKey); + if (!locked) { + // 如果获取锁失败,等待并重试查询 + return waitAndGetDevice(normalizedDeviceId); + } + + // 第三步:加锁后再次查询 + existingDevice = deviceMapper.selectOne( + new LambdaQueryWrapper().eq(Device::getDeviceId, normalizedDeviceId)); + if (existingDevice != null) { + return existingDevice; + } + + // 第四步:创建设备(使用防重复插入策略) + return createDeviceSafely(normalizedDeviceId, initializer); + + } finally { + if (locked) { + releaseLock(lockKey); + } + } + } + + /** + * 安全创建设备 - 使用数据库唯一约束作为最终保障 + */ + private Device createDeviceSafely(String deviceId, Consumer initializer) { + Device newDevice = createNewDevice(deviceId, initializer); + + try { + // 尝试插入 + int result = deviceMapper.insert(newDevice); + if (result > 0) { + log.info("设备创建成功: {}", deviceId); + return newDevice; + } + } catch (DuplicateKeyException e) { + log.debug("设备已存在(重复键异常): {}", deviceId); + // 捕获重复键异常,查询已存在的设备 + Device existingDevice = deviceMapper.selectOne( + new LambdaQueryWrapper().eq(Device::getDeviceId, deviceId)); + if (existingDevice != null) { + return existingDevice; + } + // 如果查询不到,可能是并发问题,等待后重试 + return waitAndGetDevice(deviceId); + } catch (Exception e) { + log.error("创建设备异常: {}", deviceId, e); + throw new RuntimeException("设备创建失败: " + e.getMessage(), e); + } + + throw new RuntimeException("设备创建失败,未知原因"); + } + + /** + * 等待并获取设备(用于并发场景) + */ + private Device waitAndGetDevice(String deviceId) { + // 短暂等待,让其他事务完成 + try { + TimeUnit.MILLISECONDS.sleep(100 + (long)(Math.random() * 100)); // 100-200ms随机等待 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + Device device = deviceMapper.selectOne( + new LambdaQueryWrapper().eq(Device::getDeviceId, deviceId)); + if (device != null) { + log.debug("等待后获取到设备: {}", deviceId); + return device; + } + + throw new RuntimeException("设备创建失败,等待后仍无法获取设备: " + deviceId); + } + + /** + * 获取分布式锁 + */ + private boolean tryLock(String lockKey) { + for (int i = 0; i < 3; i++) { // 重试3次 + try { + Boolean acquired = redisTemplate.opsForValue().setIfAbsent( + lockKey, + "1", + java.time.Duration.ofSeconds(LOCK_EXPIRE_SECONDS) + ); + + if (acquired != null && acquired) { + return true; + } + + // 等待后重试 + if (i < 2) { + TimeUnit.MILLISECONDS.sleep(50); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } catch (Exception e) { + log.warn("获取锁异常,重试: {}", i + 1, e); + } + } + return false; + } + + /** + * 释放锁 + */ + private void releaseLock(String lockKey) { + try { + redisTemplate.delete(lockKey); + } catch (Exception e) { + log.warn("释放锁失败: {}", lockKey, e); + } + } + + /** + * 创建新设备对象 + */ + private Device createNewDevice(String deviceId, Consumer initializer) { + Device device = new Device(); + device.setDeviceId(deviceId); + device.setDeviceName(deviceId); + device.setCreateTime(new Date()); + device.setCreateBy("系统自动创建"); + device.setIsDeleted(0); + device.setStatus("OFFLINE"); + + // 初始化运行时长统计 + device.setTotalOnlineDuration(0L); + device.setDailyDuration(0L); + device.setWeeklyDuration(0L); + device.setMonthlyDuration(0L); + + // 应用自定义初始化逻辑 + if (initializer != null) { + initializer.accept(device); + } + + return device; + } + + /** + * 统一的设备ID格式化方法 + */ + public String normalizeDeviceId(String deviceId) { + if (StrUtil.isBlank(deviceId)) { + return deviceId; + } + return deviceId.replaceAll(".*_", ""); + } + + // 在 DeviceManagerService 中添加重试方法 + public Device getOrCreateDeviceWithRetry(String originalDeviceId, Consumer initializer) { + String normalizedDeviceId = normalizeDeviceId(originalDeviceId); + + for (int retry = 0; retry < 3; retry++) { + try { + return getOrCreateDevice(normalizedDeviceId, initializer); + } catch (Exception e) { + if (retry == 2) { // 最后一次重试仍然失败 + log.error("设备创建重试失败,设备ID: {},重试次数: {}", normalizedDeviceId, retry + 1, e); + throw e; + } + + log.warn("设备创建失败,准备重试,设备ID: {},重试次数: {}", normalizedDeviceId, retry + 1); + try { + TimeUnit.MILLISECONDS.sleep(100 * (retry + 1)); // 递增等待 + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException("设备创建被中断", ie); + } + } + } + + throw new RuntimeException("设备创建失败,重试次数用完"); + } +} diff --git a/storm-device/src/main/java/com/storm/device/manager/DeviceRuntimeStatsService.java b/storm-device/src/main/java/com/storm/device/manager/DeviceRuntimeStatsService.java new file mode 100644 index 0000000..1c04122 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/manager/DeviceRuntimeStatsService.java @@ -0,0 +1,181 @@ +package com.storm.device.manager; + +import com.storm.device.domain.po.DeviceRuntimeStats; +import com.storm.device.mapper.DeviceRuntimeStatsMapper; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; + +@Slf4j +@Service +public class DeviceRuntimeStatsService { + + @Autowired + private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper; + + /** + * 安全更新运行统计 - 多层保护 + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public boolean safeUpdateRuntimeStats(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + for (int attempt = 0; attempt < 3; attempt++) { + try { + return doUpdateRuntimeStats(deviceId, iotMsgNotifyData, attempt); + } catch (DuplicateKeyException e) { + log.warn("运行统计更新重复键异常,尝试次数: {},设备: {}", attempt + 1, deviceId); + if (attempt == 2) { + log.error("运行统计更新最终失败,设备: {}", deviceId, e); + return false; + } + // 等待后重试 + try { + Thread.sleep(100 * (attempt + 1)); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return false; + } + } catch (Exception e) { + log.error("运行统计更新异常,尝试次数: {},设备: {}", attempt + 1, deviceId, e); + if (attempt == 2) { + return false; + } + } + } + return false; + } + + private boolean doUpdateRuntimeStats(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData, int attempt) { + Date today = new Date(); + Long durationToAdd = calculateMassageDuration(iotMsgNotifyData); + Integer onlineCountToAdd = shouldCountOnline(iotMsgNotifyData) ? 1 : 0; + + // 方法1: 尝试使用原子更新 + if (attempt == 0) { + try { + int result = deviceRuntimeStatsMapper.atomicUpdateStats( + deviceId, today, durationToAdd, durationToAdd, durationToAdd, onlineCountToAdd); + if (result > 0) { + log.debug("原子更新运行统计成功: {}", deviceId); + return true; + } + } catch (Exception e) { + log.debug("原子更新失败,尝试其他方法: {}", deviceId); + } + } + + // 方法2: 尝试使用 INSERT ... ON DUPLICATE KEY UPDATE + if (attempt <= 1) { + try { + DeviceRuntimeStats stats = createStatsObject(deviceId, today, durationToAdd, onlineCountToAdd); + int result = deviceRuntimeStatsMapper.insertOrUpdate(stats); + if (result > 0) { + log.debug("插入或更新运行统计成功: {}", deviceId); + return true; + } + } catch (DuplicateKeyException e) { + // 继续尝试下一种方法 + if (attempt == 1) { + throw e; // 最后一次尝试时抛出异常 + } + } catch (Exception e) { + log.debug("插入或更新失败: {}", deviceId); + } + } + + // 方法3: 使用查询+更新(最安全但性能较差) + return fallbackUpdate(deviceId, today, durationToAdd, onlineCountToAdd); + } + + private DeviceRuntimeStats createStatsObject(String deviceId, Date statDate, Long duration, Integer onlineCount) { + DeviceRuntimeStats stats = new DeviceRuntimeStats(); + stats.setDeviceId(deviceId); + stats.setStatDate(statDate); + stats.setDailyDuration(duration); + stats.setWeeklyDuration(duration); + stats.setMonthlyDuration(duration); + stats.setOnlineCount(onlineCount); + stats.setCreateTime(new Date()); + stats.setUpdateTime(new Date()); + stats.setCreateBy("系统自动创建"); + return stats; + } + + private boolean fallbackUpdate(String deviceId, Date statDate, Long durationToAdd, Integer onlineCountToAdd) { + try { + // 使用行级锁查询 + DeviceRuntimeStats existingStats = deviceRuntimeStatsMapper.selectByDeviceAndDateForUpdate(deviceId, statDate); + + if (existingStats == null) { + // 插入新记录 + DeviceRuntimeStats newStats = createStatsObject(deviceId, statDate, durationToAdd, onlineCountToAdd); + deviceRuntimeStatsMapper.insert(newStats); + } else { + // 更新现有记录 + existingStats.setDailyDuration(existingStats.getDailyDuration() + durationToAdd); + existingStats.setWeeklyDuration(existingStats.getWeeklyDuration() + durationToAdd); + existingStats.setMonthlyDuration(existingStats.getMonthlyDuration() + durationToAdd); + existingStats.setOnlineCount(existingStats.getOnlineCount() + onlineCountToAdd); + existingStats.setUpdateTime(new Date()); + deviceRuntimeStatsMapper.updateById(existingStats); + } + return true; + } catch (Exception e) { + log.error("回退方案更新运行统计失败: {}", deviceId, e); + return false; + } + } + + private Long calculateMassageDuration(IotMsgNotifyDataPro iotMsgNotifyData) { + // 实现计算按摩时长的逻辑 + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return 0L; + } + + return iotMsgNotifyData.getBody().getServices().stream() + .filter(service -> "StatusChange".equals(service.getServiceId())) + .filter(service -> service.getProperties() != null) + .mapToLong(service -> { + Object startTimeObj = service.getProperties().get("massage_start_time"); + Object endTimeObj = service.getProperties().get("massage_end_time"); + + if (startTimeObj != null && endTimeObj != null) { + try { + Long startTime = parseTimestamp(startTimeObj); + Long endTime = parseTimestamp(endTimeObj); + if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { + return (endTime - startTime) / 1000; // 转换为秒 + } + } catch (Exception e) { + log.warn("计算按摩时长失败", e); + } + } + return 0L; + }) + .sum(); + } + + private boolean shouldCountOnline(IotMsgNotifyDataPro iotMsgNotifyData) { + return iotMsgNotifyData.getBody() != null && + "ONLINE".equalsIgnoreCase(iotMsgNotifyData.getBody().getStatus()); + } + + private Long parseTimestamp(Object timestampObj) { + if (timestampObj == null) return null; + try { + if (timestampObj instanceof Number) { + return ((Number) timestampObj).longValue(); + } else if (timestampObj instanceof String) { + return Long.parseLong(timestampObj.toString()); + } + } catch (Exception e) { + log.warn("时间戳解析失败: {}", timestampObj); + } + return null; + } +} diff --git a/storm-device/src/main/java/com/storm/device/manager/MessageDeduplicationService.java b/storm-device/src/main/java/com/storm/device/manager/MessageDeduplicationService.java new file mode 100644 index 0000000..8a77e55 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/manager/MessageDeduplicationService.java @@ -0,0 +1,45 @@ +package com.storm.device.manager; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +@Service +public class MessageDeduplicationService { + + @Resource + private RedisTemplate redisTemplate; + + private static final String DEDUP_PREFIX = "msg:dedup:"; + private static final int DEDUP_EXPIRE_HOURS = 24; + + /** + * 检查消息是否重复 + */ + public boolean isDuplicate(String deviceId, String messageKey) { + String redisKey = DEDUP_PREFIX + deviceId + ":" + messageKey; + Boolean exists = redisTemplate.hasKey(redisKey); + if (exists != null && exists) { + return true; + } + // 设置过期时间 + redisTemplate.opsForValue().set(redisKey, "1", DEDUP_EXPIRE_HOURS, TimeUnit.HOURS); + return false; + } + + /** + * 生成按摩任务消息唯一键 + */ + public String generateMassageTaskKey(String deviceId, Long startTime, String headType, String bodyPart) { + return String.format("massage:%s:%d:%s:%s", deviceId, startTime, headType, bodyPart); + } + + /** + * 生成状态消息唯一键 + */ + public String generateStatusKey(String deviceId, Long eventTime, String status) { + return String.format("status:%s:%d:%s", deviceId, eventTime, status); + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/mapper/DeviceHeadUsageMapper.java b/storm-device/src/main/java/com/storm/device/mapper/DeviceHeadUsageMapper.java new file mode 100644 index 0000000..620a305 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/mapper/DeviceHeadUsageMapper.java @@ -0,0 +1,70 @@ +package com.storm.device.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.storm.device.domain.po.DeviceHeadUsage; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 设备按头使用统计Mapper接口 + * + * @author storm + * @date 2025-09-25 + */ +@Mapper +public interface DeviceHeadUsageMapper extends BaseMapper +{ + // 在DeviceHeadUsageMapper.java中添加 + @Update("${sql}") + void executeNativeSQL(@Param("sql") String sql); + /** + * 查询设备按头使用统计 + * + * @param id 设备按头使用统计主键 + * @return 设备按头使用统计 + */ + public DeviceHeadUsage selectDeviceHeadUsageById(Long id); + + /** + * 查询设备按头使用统计列表 + * + * @param deviceHeadUsage 设备按头使用统计 + * @return 设备按头使用统计集合 + */ + public List selectDeviceHeadUsageList(DeviceHeadUsage deviceHeadUsage); + + /** + * 新增设备按头使用统计 + * + * @param deviceHeadUsage 设备按头使用统计 + * @return 结果 + */ + public int insertDeviceHeadUsage(DeviceHeadUsage deviceHeadUsage); + + /** + * 修改设备按头使用统计 + * + * @param deviceHeadUsage 设备按头使用统计 + * @return 结果 + */ + public int updateDeviceHeadUsage(DeviceHeadUsage deviceHeadUsage); + + /** + * 删除设备按头使用统计 + * + * @param id 设备按头使用统计主键 + * @return 结果 + */ + public int deleteDeviceHeadUsageById(Long id); + + /** + * 批量删除设备按头使用统计 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteDeviceHeadUsageByIds(Long[] ids); +} diff --git a/storm-device/src/main/java/com/storm/device/mapper/DeviceMapper.java b/storm-device/src/main/java/com/storm/device/mapper/DeviceMapper.java new file mode 100644 index 0000000..354c6a3 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/mapper/DeviceMapper.java @@ -0,0 +1,46 @@ +package com.storm.device.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.*; + +import java.util.List; +import com.storm.device.domain.po.Device; + +/** + * 设备基本信息Mapper接口 + * + * @author storm + * @date 2025-08-22 + */ +@Mapper +public interface DeviceMapper extends BaseMapper +{ + /** + * 查询设备基本信息列表 + * + * @param device 设备基本信息 + * @return 设备基本信息集合 + */ + List selectDeviceList(Device device); + + /** + * 使用INSERT IGNORE插入设备(避免重复键异常) + */ + @Insert("INSERT IGNORE INTO device (device_id, device_name, product_id, status, " + + "total_online_duration, daily_duration, weekly_duration, monthly_duration, " + + "is_deleted, create_by, create_time) " + + "VALUES (#{deviceId}, #{deviceName}, #{productId}, #{status}, " + + "#{totalOnlineDuration}, #{dailyDuration}, #{weeklyDuration}, #{monthlyDuration}, " + + "#{isDeleted}, #{createBy}, #{createTime})") + int insertWithIgnore(Device device); + + // 在DeviceMapper.java中添加 + @Update("${sql}") + void executeNativeSQL(@Param("sql") String sql); + + /** + * 使用SELECT FOR UPDATE查询设备(行级锁) + */ + @Select("SELECT * FROM device WHERE device_id = #{deviceId} FOR UPDATE") + Device selectOneForUpdate(@Param("deviceId") String deviceId); +} diff --git a/storm-device/src/main/java/com/storm/device/mapper/DeviceRuntimeStatsMapper.java b/storm-device/src/main/java/com/storm/device/mapper/DeviceRuntimeStatsMapper.java new file mode 100644 index 0000000..2b1f88a --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/mapper/DeviceRuntimeStatsMapper.java @@ -0,0 +1,145 @@ +// DeviceRuntimeStatsMapper.java +package com.storm.device.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.storm.device.domain.po.DeviceRuntimeStats; +import com.storm.device.provider.DeviceRuntimeStatsSqlProvider; +import org.apache.ibatis.annotations.*; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Mapper +public interface DeviceRuntimeStatsMapper extends BaseMapper { + + @Select("SELECT * FROM device_runtime_stats WHERE device_id = #{deviceId} AND stat_date = #{statDate}") + DeviceRuntimeStats selectByDeviceAndDate(@Param("deviceId") String deviceId, @Param("statDate") Date statDate); + + /** + * 插入或更新设备运行统计 + */ + @Insert({""}) + boolean insertOrUpdateStats(@Param("deviceId") String deviceId, + @Param("statDate") Date statDate, + @Param("dailyDuration") Long dailyDuration, + @Param("weeklyDuration") Long weeklyDuration, + @Param("monthlyDuration") Long monthlyDuration, + @Param("onlineCount") Integer onlineCount, + @Param("createTime") Date createTime, + @Param("updateTime") Date updateTime, + @Param("createBy") String createBy); + + @Select("SELECT stat_date as date, daily_duration as duration FROM device_runtime_stats " + + "WHERE device_id = #{deviceId} AND stat_date BETWEEN #{startDate} AND #{endDate} " + + "ORDER BY stat_date") + List> selectDailyStatsByPeriod(@Param("deviceId") String deviceId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); + + // 在 DeviceRuntimeStatsMapper.java 中添加 + @Insert("INSERT INTO device_runtime_stats (device_id, stat_date, daily_duration, weekly_duration, " + + "monthly_duration, online_count, create_time, update_time, create_by) " + + "VALUES (#{deviceId}, #{statDate}, #{dailyDuration}, #{weeklyDuration}, " + + "#{monthlyDuration}, #{onlineCount}, #{createTime}, #{updateTime}, #{createBy}) " + + "ON DUPLICATE KEY UPDATE " + + "daily_duration = daily_duration + VALUES(daily_duration), " + + "weekly_duration = weekly_duration + VALUES(weekly_duration), " + + "monthly_duration = monthly_duration + VALUES(monthly_duration), " + + "online_count = online_count + VALUES(online_count), " + + "update_time = VALUES(update_time)") + int insertOrUpdate(DeviceRuntimeStats stats); + + // 批量插入或更新 + @Insert("") + int batchInsertOrUpdate(@Param("list") List statsList); + + @Select("SELECT SUM(daily_duration) as totalDuration FROM device_runtime_stats " + + "WHERE device_id = #{deviceId} AND stat_date BETWEEN #{startDate} AND #{endDate}") + Long selectTotalDurationByPeriod(@Param("deviceId") String deviceId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); + + // 在 DeviceRuntimeStatsMapper.java 中添加更健壮的查询方法 + @Select("SELECT * FROM device_runtime_stats WHERE device_id = #{deviceId} AND stat_date = #{statDate} FOR UPDATE") + DeviceRuntimeStats selectByDeviceAndDateForUpdate(@Param("deviceId") String deviceId, + @Param("statDate") Date statDate); + + /** + * 原子性更新方法 + */ + @Update("UPDATE device_runtime_stats SET " + + "daily_duration = daily_duration + #{dailyDuration}, " + + "weekly_duration = weekly_duration + #{weeklyDuration}, " + + "monthly_duration = monthly_duration + #{monthlyDuration}, " + + "online_count = online_count + #{onlineCount}, " + + "update_time = NOW() " + + "WHERE device_id = #{deviceId} AND stat_date = #{statDate}") + int atomicUpdateStats(@Param("deviceId") String deviceId, + @Param("statDate") Date statDate, + @Param("dailyDuration") Long dailyDuration, + @Param("weeklyDuration") Long weeklyDuration, + @Param("monthlyDuration") Long monthlyDuration, + @Param("onlineCount") Integer onlineCount); + + // 1. 查询时间段内的上线次数 + @Select("SELECT COUNT(DISTINCT DATE(event_time)) " + + "FROM device_status_log " + + "WHERE device_id = #{deviceId} " + + "AND event_time >= #{startDate} " + + "AND event_time <= #{endDate} " + + "AND status = 'ONLINE'") + Integer selectOnlineCountByPeriod(@Param("deviceId") String deviceId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); + + // 2. 更新周统计时长 + @Update("UPDATE device_runtime_stats " + + "SET weekly_duration = weekly_duration + #{duration} " + + "WHERE device_id = #{deviceId} " + + "AND stat_date BETWEEN #{weekStart} AND #{statDate}") + void updateWeeklyDuration(@Param("deviceId") String deviceId, + @Param("weekStart") Date weekStart, + @Param("statDate") Date statDate, + @Param("duration") Long duration); + + // 3. 更新月统计时长 + @Update("UPDATE device_runtime_stats " + + "SET monthly_duration = monthly_duration + #{duration} " + + "WHERE device_id = #{deviceId} " + + "AND stat_date BETWEEN #{monthStart} AND #{statDate}") + void updateMonthlyDuration(@Param("deviceId") String deviceId, + @Param("monthStart") Date monthStart, + @Param("statDate") Date statDate, + @Param("duration") Long duration); + + // 4. 查询设备运行统计列表 + @SelectProvider(type = DeviceRuntimeStatsSqlProvider.class, method = "selectDeviceRuntimeStatsList") + List selectDeviceRuntimeStatsList(DeviceRuntimeStats deviceRuntimeStats); + + DeviceRuntimeStats selectTodayStats(String deviceId); +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/mapper/DeviceStatusLogMapper.java b/storm-device/src/main/java/com/storm/device/mapper/DeviceStatusLogMapper.java new file mode 100644 index 0000000..d7c1898 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/mapper/DeviceStatusLogMapper.java @@ -0,0 +1,122 @@ +package com.storm.device.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.storm.device.domain.po.DeviceStatusLog; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +public interface DeviceStatusLogMapper extends BaseMapper { + + /** + * 查询设备状态历史 + */ + @Select("SELECT id, device_id, status, event_time, last_online_time, duration " + + "FROM device_status_log " + + "WHERE device_id = #{deviceId} " + + "ORDER BY event_time DESC " + + "LIMIT #{limit}") + List> selectStatusHistory(@Param("deviceId") String deviceId, + @Param("limit") int limit); + + /** + * 查询设备按摩历史 + */ + @Select("SELECT id, device_id, head_type, body_part, massage_plan, " + + "massage_start_time, massage_end_time, massage_duration, event_time " + + "FROM device_status_log " + + "WHERE device_id = #{deviceId} AND event_type = 'MASSAGE_TASK' " + + "ORDER BY massage_start_time DESC " + + "LIMIT #{limit}") + List> selectMassageHistory(@Param("deviceId") String deviceId, + @Param("limit") int limit); + + /** + * 查询设备按摩统计 + */ + @Select("") + List> selectMassageStatsByDeviceAndPeriod( + @Param("deviceId") String deviceId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); + + /** + * 查询按头类型统计 + */ + @Select("") + List> selectHeadTypeStatsByDevice( + @Param("deviceId") String deviceId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); + + /** + * 查询按部位统计 + */ + @Select("") + List> selectBodyPartStatsByDevice( + @Param("deviceId") String deviceId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); + + List> selectMassageTrend(String deviceId, Integer limit); + + // 在 DeviceStatusLogMapper.java 中添加 + @Insert("INSERT IGNORE INTO device_status_log (device_id, status, event_type, event_time, " + + "last_online_time, duration, create_time, update_time, create_by) " + + "VALUES (#{deviceId}, #{status}, #{eventType}, #{eventTime}, " + + "#{lastOnlineTime}, #{duration}, #{createTime}, #{updateTime}, #{createBy})") + int insertOrIgnore(DeviceStatusLog deviceStatusLog); + + @Insert("INSERT INTO device_status_log (device_id, status, event_type, event_time, " + + "last_online_time, duration, create_time, update_time, create_by) " + + "VALUES (#{deviceId}, #{status}, #{eventType}, #{eventTime}, " + + "#{lastOnlineTime}, #{duration}, #{createTime}, #{updateTime}, #{createBy}) " + + "ON DUPLICATE KEY UPDATE " + + "last_online_time = VALUES(last_online_time), " + + "duration = VALUES(duration), " + + "update_time = VALUES(update_time)") + int insertOrUpdate(DeviceStatusLog deviceStatusLog); + + // 在 DeviceStatusLogMapper.java 中添加 + @Insert("") + int batchInsertIgnore(@Param("list") java.util.List statusLogs); +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/mapper/MassageTaskMapper.java b/storm-device/src/main/java/com/storm/device/mapper/MassageTaskMapper.java new file mode 100644 index 0000000..546657f --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/mapper/MassageTaskMapper.java @@ -0,0 +1,44 @@ +package com.storm.device.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.storm.device.domain.dto.MassageTaskStatDTO; +import com.storm.device.domain.po.MassageTask; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 按摩任务记录Mapper接口 + * + * @author storm + * @date 2025-08-28 + */ +@Mapper +public interface MassageTaskMapper extends BaseMapper { + + List> countByHeadType(@Param("startTime") String startTime, @Param("endTime") String endTime); + + List> countByBodyPart(@Param("startTime") String startTime, @Param("endTime") String endTime); + + List getMassageTaskStatsByRange(@Param("startTime") String startTime, @Param("endTime") String endTime); + + List> getDeviceMassageStats(@Param("startTime") String startTime, @Param("endTime") String endTime); + + String getMinDate(); + + List> getDailyStats(@Param("startTime") String startTime, @Param("endTime") String endTime); + + String getPeakDate(@Param("startTime") String startTime, @Param("endTime") String endTime); + + Double getTotalDuration(@Param("startTime") String startTime, @Param("endTime") String endTime); + + Map getStats(); + + Long getMassageTaskTotal(); + + Long getReportEvent(); + + List selectMassageTaskList(MassageTask massageTask); +} diff --git a/storm-device/src/main/java/com/storm/device/processor/DeviceDataCoordinator.java b/storm-device/src/main/java/com/storm/device/processor/DeviceDataCoordinator.java new file mode 100644 index 0000000..940e379 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/processor/DeviceDataCoordinator.java @@ -0,0 +1,666 @@ +package com.storm.device.processor; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.storm.common.core.utils.StringUtils; +import com.storm.device.domain.po.*; +import com.storm.device.manager.DeviceManagerService; +import com.storm.device.manager.DeviceRuntimeStatsService; +import com.storm.device.mapper.*; +import com.storm.device.service.IDeviceStatusLogService; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import java.util.Date; +import java.util.Map; +import java.util.Objects; + +@Slf4j +@Component +public class DeviceDataCoordinator { + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private MassageTaskMapper massageTaskMapper; + + @Autowired + private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper; + + @Autowired + private DeviceHeadUsageMapper deviceHeadUsageMapper; + + @Autowired + private DeviceManagerService deviceManagerService; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IDeviceStatusLogService deviceStatusLogService; + + private static final String SYNC_LOCK_PREFIX = "device:sync:lock:"; + + @Autowired + private DeviceRuntimeStatsService deviceRuntimeStatsService; + + /** + * 确保设备存在 + */ + private Device ensureDeviceExists(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + return deviceManagerService.getOrCreateDevice(deviceId, (device) -> { + // 初始化设备信息 + if (iotMsgNotifyData.getHeader() != null) { + device.setProductId(iotMsgNotifyData.getHeader().getProductId()); + } + if (iotMsgNotifyData.getBody() != null) { + device.setStatus(iotMsgNotifyData.getBody().getStatus()); + } + device.setCreateTime(new Date()); + device.setCreateBy("系统自动创建"); + }); + } + + /** + * 处理按摩任务数据 + */ + private void processMassageTaskData(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + + if (!isValidMassageTaskData(device.getDeviceId(), props)) { + continue; + } + + // 检查是否重复 + if (isMassageTaskExists(device.getDeviceId(), props)) { + log.debug("按摩任务已存在,跳过: {}", device.getDeviceId()); + continue; + } + + MassageTask task = createMassageTask(device, props); + if (task != null) { + massageTaskMapper.insert(task); + log.info("按摩任务记录成功: 设备{},时长{}秒", device.getDeviceId(), task.getTaskTime()); + } + break; + } + } + } + + /** + * 更新按头使用统计 + */ + private void updateHeadUsageStatistics(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + String headType = safeGetString(props, "head_type"); + + if (StrUtil.isBlank(headType) || !isValidMassageTaskData(device.getDeviceId(), props)) { + continue; + } + + Long duration = calculateMassageDuration(iotMsgNotifyData); + if (duration <= 0) { + continue; + } + + DeviceHeadUsage usage = deviceHeadUsageMapper.selectOne( + new LambdaQueryWrapper() + .eq(DeviceHeadUsage::getDeviceId, device.getDeviceId()) + .eq(DeviceHeadUsage::getHeadType, headType)); + + if (usage == null) { + usage = new DeviceHeadUsage(); + usage.setDeviceId(device.getDeviceId()); + usage.setHeadType(headType); + usage.setUsageCount(0); + usage.setTotalDuration(0L); + usage.setDailyDuration(0L); + usage.setWeeklyDuration(0L); + usage.setMonthlyDuration(0L); + usage.setCreateBy("系统自动创建"); + usage.setCreateTime(new Date()); + } + + usage.setUsageCount(usage.getUsageCount() + 1); + usage.setTotalDuration(usage.getTotalDuration() + duration); + usage.setDailyDuration(usage.getDailyDuration() + duration); + usage.setWeeklyDuration(usage.getWeeklyDuration() + duration); + usage.setMonthlyDuration(usage.getMonthlyDuration() + duration); + usage.setLastUsedTime(new Date()); + usage.setUpdateTime(new Date()); + usage.setUpdateBy("系统自动修改"); + if (usage.getId() == null) { + deviceHeadUsageMapper.insert(usage); + } else { + deviceHeadUsageMapper.updateById(usage); + } + + break; + } + } + } catch (Exception e) { + log.error("更新按头使用统计失败,设备: {}", device.getDeviceId(), e); + } + } + + /** + * 更新设备总统计 + */ + private void updateDeviceTotalStatistics(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + Long duration = calculateMassageDuration(iotMsgNotifyData); + if (duration > 0) { + // 更新设备表中的累计时长 + device.setTotalOnlineDuration(device.getTotalOnlineDuration() + duration); + + // 更新日统计 + if (isSameDay(device.getUpdateTime(), new Date())) { + device.setDailyDuration(device.getDailyDuration() + duration); + } else { + device.setDailyDuration(duration); + } + + // 更新周、月统计(简化处理,实际应该按周月重置) + device.setWeeklyDuration(device.getWeeklyDuration() + duration); + device.setMonthlyDuration(device.getMonthlyDuration() + duration); + device.setUpdateBy("系统自动修改"); + device.setUpdateTime(new Date()); + deviceMapper.updateById(device); + } + } catch (Exception e) { + log.error("更新设备总统计失败,设备: {}", device.getDeviceId(), e); + } + } + + // ========== 辅助方法 ========== + + private boolean acquireLock(String lockKey) { + try { + Boolean acquired = redisTemplate.opsForValue().setIfAbsent( + lockKey, "1", java.time.Duration.ofSeconds(10)); + return acquired != null && acquired; + } catch (Exception e) { + log.warn("获取锁失败: {}", lockKey, e); + return false; + } + } + + private void releaseLock(String lockKey) { + try { + redisTemplate.delete(lockKey); + } catch (Exception e) { + log.warn("释放锁失败: {}", lockKey, e); + } + } + + private DeviceStatusLog createStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + // 实现创建状态日志的逻辑(从您现有的代码中提取) + DeviceStatusLog statusLog = new DeviceStatusLog(); + statusLog.setDeviceId(deviceId); + statusLog.setCreateTime(new Date()); + + if (iotMsgNotifyData.getBody() != null) { + String status = iotMsgNotifyData.getBody().getStatus(); + statusLog.setStatus("ONLINE".equalsIgnoreCase(status) ? "ONLINE" : "OFFLINE"); + statusLog.setEventTime(iotMsgNotifyData.getBody().getStatusUpdateTime() != null ? + iotMsgNotifyData.getBody().getStatusUpdateTime() : new Date()); + } + + return statusLog; + } + + private boolean isValidMassageTaskData(String deviceId, Map props) { + return props.containsKey("massage_start_time") && + props.containsKey("massage_end_time") && + props.containsKey("head_type") && + props.containsKey("body_part"); + } + + private boolean isMassageTaskExists(String deviceId, Map props) { + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + String headType = safeGetString(props, "head_type"); + + if (startTime == null || StrUtil.isBlank(headType)) { + return false; + } + + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(MassageTask::getDeviceId, deviceId) + .eq(MassageTask::getStartTime, startTime) + .eq(MassageTask::getHeadType, headType); + + return massageTaskMapper.selectCount(query) > 0; + } + + private MassageTask createMassageTask(Device device, Map props) { + try { + MassageTask task = new MassageTask(); + task.setDeviceId(device.getDeviceId()); + task.setDeviceName(device.getDeviceName()); + task.setProductName(device.getProductName()); + task.setHeadType(safeGetString(props, "head_type")); + task.setBodyPart(safeGetString(props, "body_part")); + task.setMassagePlan(safeGetString(props, "massage_plan")); + task.setCreateBy("系统自动创建"); + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + task.setCreateTimestamp(new Date().getTime()); + if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { + task.setStartTime(startTime); + task.setEndTime(endTime); + task.setTaskTime((endTime - startTime) / 1000); + task.setCreateTime(new Date()); + return task; + } + } catch (Exception e) { + log.error("创建按摩任务失败", e); + } + return null; + } + + private Long calculateMassageDuration(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return 0L; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + + if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { + return (endTime - startTime) / 1000; // 转换为秒 + } + break; + } + } + return 0L; + } + + private boolean shouldCountOnline(IotMsgNotifyDataPro iotMsgNotifyData) { + return iotMsgNotifyData.getBody() != null && + "ONLINE".equalsIgnoreCase(iotMsgNotifyData.getBody().getStatus()); + } + + private void updateDeviceTotalDuration(Device device, Long duration) { + if (duration > 0) { + device.setTotalOnlineDuration(device.getTotalOnlineDuration() + duration); + deviceMapper.updateById(device); + } + } + + private boolean isSameDay(Date date1, Date date2) { + if (date1 == null || date2 == null) return false; + java.util.Calendar cal1 = java.util.Calendar.getInstance(); + java.util.Calendar cal2 = java.util.Calendar.getInstance(); + cal1.setTime(date1); + cal2.setTime(date2); + return cal1.get(java.util.Calendar.YEAR) == cal2.get(java.util.Calendar.YEAR) && + cal1.get(java.util.Calendar.DAY_OF_YEAR) == cal2.get(java.util.Calendar.DAY_OF_YEAR); + } + + private String safeGetString(Map props, String key) { + if (props == null || !props.containsKey(key)) return null; + Object value = props.get(key); + return value != null ? value.toString() : null; + } + + private Long safeParseTimestamp(Object timestampObj) { + if (timestampObj == null) return null; + try { + if (timestampObj instanceof Number) { + return ((Number) timestampObj).longValue(); + } else if (timestampObj instanceof String) { + return Long.parseLong(timestampObj.toString()); + } + } catch (Exception e) { + log.warn("时间戳解析失败: {}", timestampObj); + } + return null; + } + + /** + * 备用方案:先查询后更新 + */ + private void fallbackUpdateRuntimeStatistics(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + Date today = new Date(); + + // 先查询是否存在 + DeviceRuntimeStats existingStats = deviceRuntimeStatsMapper.selectByDeviceAndDate(device.getDeviceId(), today); + + Long durationToAdd = calculateMassageDuration(iotMsgNotifyData); + Integer onlineCountToAdd = shouldCountOnline(iotMsgNotifyData) ? 1 : 0; + + if (existingStats == null) { + // 插入新记录 + DeviceRuntimeStats newStats = new DeviceRuntimeStats(); + newStats.setDeviceId(device.getDeviceId()); + newStats.setStatDate(today); + newStats.setDailyDuration(durationToAdd); + newStats.setWeeklyDuration(durationToAdd); + newStats.setMonthlyDuration(durationToAdd); + newStats.setOnlineCount(onlineCountToAdd); + newStats.setCreateTime(new Date()); + newStats.setUpdateTime(new Date()); + newStats.setCreateBy("系统自动创建"); + + deviceRuntimeStatsMapper.insert(newStats); + } else { + // 更新现有记录 + existingStats.setDailyDuration(existingStats.getDailyDuration() + durationToAdd); + existingStats.setWeeklyDuration(existingStats.getWeeklyDuration() + durationToAdd); + existingStats.setMonthlyDuration(existingStats.getMonthlyDuration() + durationToAdd); + existingStats.setOnlineCount(existingStats.getOnlineCount() + onlineCountToAdd); + existingStats.setUpdateTime(new Date()); + + deviceRuntimeStatsMapper.updateById(existingStats); + } + + log.info("备用方案更新运行统计成功: {}", device.getDeviceId()); + + } catch (Exception e) { + log.error("备用方案更新运行统计也失败,设备: {}", device.getDeviceId(), e); + } + } + + /** + * 处理设备状态变更 - 最终修复版本 + */ + private void processDeviceStatusChange(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null) { + return; + } + + // 更新设备状态 + String newStatus = iotMsgNotifyData.getBody().getStatus(); + if (newStatus != null) { + String normalizedStatus = "ONLINE".equalsIgnoreCase(newStatus) ? "ONLINE" : "OFFLINE"; + device.setStatus(normalizedStatus); + + Date now = new Date(); + if ("ONLINE".equals(normalizedStatus)) { + device.setLastOnlineTime(now); + } else { + device.setLastOfflineTime(now); + } + + device.setUpdateTime(now); + deviceMapper.updateById(device); + + log.debug("设备状态更新: {} -> {}", device.getDeviceId(), normalizedStatus); + } + + // 使用专门的服务处理状态日志插入 + boolean success = deviceStatusLogService.safeInsertStatusLog(device.getDeviceId(), iotMsgNotifyData); + + if (success) { + log.debug("状态日志处理成功: {}", device.getDeviceId()); + } else { + log.warn("状态日志处理失败: {}", device.getDeviceId()); + } + } + + /** + * 更新设备位置信息 - 新增方法 + */ + private void updateDeviceLocation(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return; + } + + boolean locationUpdated = false; + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if (service.getProperties() == null) continue; + + Map props = service.getProperties(); + + // 检查是否有新的位置信息 + boolean hasNewLongitude = props.containsKey("longitude") && props.get("longitude") != null; + boolean hasNewLatitude = props.containsKey("latitude") && props.get("latitude") != null; + + if (hasNewLongitude || hasNewLatitude) { + String newLongitude = hasNewLongitude ? props.get("longitude").toString() : null; + String newLatitude = hasNewLatitude ? props.get("latitude").toString() : null; + + // 检查位置是否发生变化 + boolean longitudeChanged = !Objects.equals(device.getLongitude(), newLongitude); + boolean latitudeChanged = !Objects.equals(device.getLatitude(), newLatitude); + + if (longitudeChanged || latitudeChanged) { + if (hasNewLongitude) { + device.setLongitude(newLongitude); + log.debug("更新设备经度: {} -> {}", device.getDeviceId(), newLongitude); + } + if (hasNewLatitude) { + device.setLatitude(newLatitude); + log.debug("更新设备纬度: {} -> {}", device.getDeviceId(), newLatitude); + } + + locationUpdated = true; + + // 如果有位置信息,尝试获取详细地址 + if (hasNewLongitude && hasNewLatitude) { + try { +// TODO 基于经纬度获取省市位置 +// String location = getLocationFromCoordinates( +// Double.parseDouble(newLatitude), +// Double.parseDouble(newLongitude) +// ); +// if (!"Unknown location".equals(location) && !"Error fetching location".equals(location)) { +// device.setLocation(location); +// log.debug("更新设备位置地址: {} -> {}", device.getDeviceId(), location); +// } + } catch (Exception e) { + log.warn("获取设备位置地址失败: {}", device.getDeviceId(), e); + } + } + } + } + + // 同时处理版本信息更新 + if (props.containsKey("software_version") && props.get("software_version") != null) { + String newSoftwareVersion = props.get("software_version").toString(); + if (!Objects.equals(device.getSoftwareVersion(), newSoftwareVersion)) { + device.setSoftwareVersion(newSoftwareVersion); + log.debug("更新软件版本: {} -> {}", device.getDeviceId(), newSoftwareVersion); + locationUpdated = true; + } + } + + if (props.containsKey("vtxdb_version") && props.get("vtxdb_version") != null) { + String newVtxdbVersion = props.get("vtxdb_version").toString(); + if (!Objects.equals(device.getVtxdbVersion(), newVtxdbVersion)) { + device.setVtxdbVersion(newVtxdbVersion); + log.debug("更新参数服务器版本: {} -> {}", device.getDeviceId(), newVtxdbVersion); + locationUpdated = true; + } + } + } + + if (locationUpdated) { + device.setUpdateTime(new Date()); + deviceMapper.updateById(device); + log.info("设备位置和版本信息更新完成: {}", device.getDeviceId()); + } + } + + /** + * 更新运行统计 - 最终修复版本 + */ + private void updateRuntimeStatistics(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + // 使用专门的服务处理运行统计更新 + boolean success = deviceRuntimeStatsService.safeUpdateRuntimeStats(device.getDeviceId(), iotMsgNotifyData); + + if (success) { + log.debug("运行统计更新成功: {}", device.getDeviceId()); + } else { + log.warn("运行统计更新失败: {}", device.getDeviceId()); + } + + } catch (Exception e) { + log.error("更新运行统计失败,设备: {}", device.getDeviceId(), e); + } + } + + /** + * 更新设备最后按摩时间 + */ + private void updateDeviceLastMassageTime(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + + // 检查是否有按摩结束时间 + if (props.containsKey("massage_end_time")) { + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + if (endTime != null && endTime > 0) { + Date lastMassageTime = new Date(endTime); + + // 只更新为更晚的时间 + if (device.getLastMassageTime() == null || + lastMassageTime.after(device.getLastMassageTime())) { + device.setLastMassageTime(lastMassageTime); + log.debug("更新设备最后按摩时间: {} -> {}", device.getDeviceId(), lastMassageTime); + } + } + } + break; + } + } + } catch (Exception e) { + log.error("更新设备最后按摩时间失败: {}", device.getDeviceId(), e); + } + } + + /** + * 统一处理设备数据 - 增强异常处理 + */ + @Transactional(rollbackFor = Exception.class) + public void coordinateDeviceData(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) { + return; + } + + String deviceId = iotMsgNotifyData.getHeader().getDeviceId(); + if (StringUtils.isBlank(deviceId)) { + return; + } + + String normalizedDeviceId = deviceManagerService.normalizeDeviceId(deviceId); + String lockKey = SYNC_LOCK_PREFIX + normalizedDeviceId; + + boolean locked = false; + try { + // 获取分布式锁 + locked = acquireLock(lockKey); + if (!locked) { + log.warn("获取设备数据同步锁失败,设备: {}", normalizedDeviceId); + return; + } + + // 1. 确保设备基础信息存在 + Device device = ensureDeviceExists(normalizedDeviceId, iotMsgNotifyData); + if (device == null) { + log.error("设备基础信息创建失败: {}", normalizedDeviceId); + return; + } + + // 2. 处理设备位置信息更新 + try { + updateDeviceLocation(device, iotMsgNotifyData); + } catch (Exception e) { + log.error("处理设备位置信息更新失败,设备: {}", normalizedDeviceId, e); + } + + // 3. 处理设备状态变更 + try { + processDeviceStatusChange(device, iotMsgNotifyData); + } catch (Exception e) { + log.error("处理设备状态变更失败,继续处理其他数据,设备: {}", normalizedDeviceId, e); + } + + // 4. 处理按摩任务数据 + try { + processMassageTaskData(device, iotMsgNotifyData); + } catch (Exception e) { + log.error("处理按摩任务数据失败,设备: {}", normalizedDeviceId, e); + } + + // 5. 更新最后按摩时间(新增) + try { + updateDeviceLastMassageTime(device, iotMsgNotifyData); + } catch (Exception e) { + log.error("更新最后按摩时间失败,设备: {}", normalizedDeviceId, e); + } + + // 6. 更新运行统计 + try { + updateRuntimeStatistics(device, iotMsgNotifyData); + } catch (Exception e) { + log.error("更新运行统计失败,设备: {}", normalizedDeviceId, e); + } + + // 7. 更新按头使用统计 + try { + updateHeadUsageStatistics(device, iotMsgNotifyData); + } catch (Exception e) { + log.error("更新按头使用统计失败,设备: {}", normalizedDeviceId, e); + } + + // 8. 更新设备总统计(移除重复的时长累加) + try { + updateDeviceBasicInfo(device); // 只更新基本信息,不重复累加时长 + } catch (Exception e) { + log.error("更新设备基本信息失败,设备: {}", normalizedDeviceId, e); + } + + log.info("设备数据同步完成: {}", normalizedDeviceId); + + } catch (Exception e) { + log.error("设备数据协调处理异常,设备: {}", normalizedDeviceId, e); + } finally { + if (locked) { + releaseLock(lockKey); + } + } + } + + /** + * 只更新设备基本信息,不重复累加时长 + */ + private void updateDeviceBasicInfo(Device device) { + try { + device.setUpdateTime(new Date()); + device.setUpdateBy("系统自动更新"); + deviceMapper.updateById(device); + } catch (Exception e) { + log.error("更新设备基本信息失败: {}", device.getDeviceId(), e); + } + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/processor/DeviceDataProcessor.java b/storm-device/src/main/java/com/storm/device/processor/DeviceDataProcessor.java new file mode 100644 index 0000000..71f5ee3 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/processor/DeviceDataProcessor.java @@ -0,0 +1,1218 @@ +package com.storm.device.processor; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.storm.device.domain.po.*; +import com.storm.device.manager.DeviceManagerService; +import com.storm.device.mapper.*; +import com.storm.device.service.IDeviceService; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +@Slf4j +@Component +public class DeviceDataProcessor { + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private DeviceStatusLogMapper deviceStatusLogMapper; + + @Autowired + private MassageTaskMapper massageTaskMapper; + + @Autowired + private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper; + + @Autowired + private DeviceHeadUsageMapper deviceHeadUsageMapper; + + @Autowired + private DeviceManagerService deviceManagerService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private RedisTemplate redisTemplate; + + private static final String DEVICE_LOCK_PREFIX = "device:lock:"; + private static final String DEVICE_EXIST_PREFIX = "device:exist:"; + + @Autowired + private DeviceDataCoordinator deviceDataCoordinator; + + /** + * 更新设备信息 + */ + private void updateDeviceInfo(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + device.setUpdateTime(new Date()); + device.setUpdateBy("系统自动更新"); + + // 处理设备状态 + if (iotMsgNotifyData.getBody() != null && StrUtil.isNotBlank(iotMsgNotifyData.getBody().getStatus())) { + String status = iotMsgNotifyData.getBody().getStatus(); + device.setStatus("ONLINE".equalsIgnoreCase(status) ? "ONLINE" : "OFFLINE"); + + Date now = new Date(); + if ("ONLINE".equalsIgnoreCase(status)) { + device.setLastOnlineTime(now); + log.debug("设备上线: {}", device.getDeviceId()); + } else if ("OFFLINE".equalsIgnoreCase(status)) { + device.setLastOfflineTime(now); + log.debug("设备下线: {}", device.getDeviceId()); + } + } + + // 处理服务数据 + if (iotMsgNotifyData.getBody() != null && iotMsgNotifyData.getBody().getServices() != null) { + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + Map properties = service.getProperties(); + if (properties == null) continue; + + String serviceId = service.getServiceId(); + switch (serviceId) { + case "robot_info": + processRobotInfo(device, properties); + break; + case "License": + processLicenseInfo(device, properties); + break; + case "StatusChange": + processStatusChangeInfo(device, properties); + break; + case "VortXDB": + processVortXDBInfo(device, properties); + break; + default: + log.debug("未知服务类型: {}", serviceId); + } + } + } + } + + // 在DeviceDataProcessor中添加设备使用时间统计调用 + private void updateMassageTaskTable(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + + // 严格检查数据完整性 + if (!isValidMassageTaskData(deviceId, props)) { + log.debug("按摩任务数据不完整或无效,跳过处理: {}", deviceId); + continue; + } + + // 检查是否已存在相同记录 + if (isMassageTaskExists(deviceId, props)) { + log.debug("按摩任务已存在,跳过处理: 设备{},开始时间{}", deviceId, props.get("massage_start_time")); + continue; + } + + MassageTask task = createMassageTask(deviceId, props); + if (task != null && isValidMassageDuration(task)) { + try { + massageTaskMapper.insert(task); + log.info("按摩任务记录已保存: 设备{},按摩头{},部位{},时长{}秒", + deviceId, task.getHeadType(), task.getBodyPart(), task.getTaskTime()); + + // 新增:调用设备使用时间统计 + updateDeviceUsageStats(deviceId, task.getTaskTime()); + + } catch (DuplicateKeyException e) { + log.debug("按摩任务记录已存在(并发情况)"); + } + } + break; + } + } + } catch (Exception e) { + log.error("更新按摩任务表时发生异常,设备ID: {}", deviceId, e); + } + } + + // 新增设备使用时间统计方法 + private void updateDeviceUsageStats(String deviceId, Long durationSeconds) { + if (durationSeconds == null || durationSeconds <= 0) { + return; + } + + try { + // 查询设备 + Device device = deviceService.getOne( + new LambdaQueryWrapper().eq(Device::getDeviceId, deviceId)); + + if (device == null) { + log.warn("设备不存在,无法更新使用时间: {}", deviceId); + return; + } + + // 更新总使用时长 + Long currentTotal = device.getTotalOnlineDuration() != null ? device.getTotalOnlineDuration() : 0L; + device.setTotalOnlineDuration(currentTotal + durationSeconds); + + // 更新今日使用时长(需要判断是否是同一天) + Date now = new Date(); + if (isSameDay(device.getUpdateTime(), now)) { + Long currentDaily = device.getDailyDuration() != null ? device.getDailyDuration() : 0L; + device.setDailyDuration(currentDaily + durationSeconds); + } else { + device.setDailyDuration(durationSeconds); + } + + // 更新本周时长(简化处理,实际应该按周统计) + Long currentWeekly = device.getWeeklyDuration() != null ? device.getWeeklyDuration() : 0L; + device.setWeeklyDuration(currentWeekly + durationSeconds); + + // 更新本月时长(简化处理,实际应该按月统计) + Long currentMonthly = device.getMonthlyDuration() != null ? device.getMonthlyDuration() : 0L; + device.setMonthlyDuration(currentMonthly + durationSeconds); + + device.setUpdateTime(now); + deviceService.updateById(device); + + log.debug("设备使用时间统计更新: {} -> 增加{}秒", deviceId, durationSeconds); + + } catch (Exception e) { + log.error("更新设备使用时间统计失败: {}", deviceId, e); + } + } + + // 判断是否为同一天 + private boolean isSameDay(Date date1, Date date2) { + if (date1 == null || date2 == null) return false; + + try { + Calendar cal1 = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal1.setTime(date1); + cal2.setTime(date2); + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && + cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR); + } catch (Exception e) { + log.warn("日期比较失败", e); + return false; + } + } + + /** + * 检查按摩任务是否已存在 + */ + private boolean isMassageTaskExists(String deviceId, Map props) { + try { + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + String headType = safeGetString(props, "head_type"); + String bodyPart = safeGetString(props, "body_part"); + + if (startTime == null || StrUtil.isBlank(headType) || StrUtil.isBlank(bodyPart)) { + return false; + } + + // 构建查询条件 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(MassageTask::getDeviceId, deviceId) + .eq(MassageTask::getStartTime, startTime) + .eq(MassageTask::getHeadType, headType) + .eq(MassageTask::getBodyPart, bodyPart); + + return massageTaskMapper.selectCount(queryWrapper) > 0; + } catch (Exception e) { + log.warn("检查按摩任务是否存在时发生异常", e); + return false; + } + } + + /** + * 创建按摩任务对象 + */ + private MassageTask createMassageTask(String deviceId, Map props) { + try { + MassageTask task = new MassageTask(); + task.setDeviceId(deviceId); + task.setCreateTime(new Date()); + + // 设置按摩任务信息 + task.setHeadType(safeGetString(props, "head_type")); + task.setBodyPart(safeGetString(props, "body_part")); + task.setMassagePlan(safeGetString(props, "massage_plan")); + + // 处理时间 + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + + if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { + task.setStartTime(startTime); + task.setEndTime(endTime); + task.setTaskTime((endTime - startTime) / 1000); // 转换为秒 + + // 查询设备信息补充设备名称 + Device device = deviceMapper.selectOne( + new LambdaQueryWrapper().eq(Device::getDeviceId, deviceId)); + if (device != null) { + task.setDeviceName(device.getDeviceName()); + task.setProductName(device.getProductName()); + } else { + task.setDeviceName(deviceId); + task.setProductName("未知产品"); + } + + task.setCreateBy("系统自动创建"); + return task; + } else { + log.warn("按摩任务时间无效,设备: {},开始时间: {},结束时间: {}", deviceId, startTime, endTime); + return null; + } + } catch (Exception e) { + log.error("创建按摩任务对象失败,设备ID: {}", deviceId, e); + return null; + } + } + + /** + * 4. 更新设备运行统计表 - 修复并发问题 + */ + private void updateRuntimeStatsTable(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + Date today = new Date(); + + // 直接尝试插入或更新,避免先查询的并发问题 + DeviceRuntimeStats stats = new DeviceRuntimeStats(); + stats.setDeviceId(deviceId); + stats.setStatDate(today); + stats.setUpdateTime(new Date()); + stats.setCreateBy("系统自动创建"); + + // 设置基础值 + Long durationToAdd = 0L; + Integer onlineCountToAdd = 0; + + // 如果是上线状态,增加在线计数 + if (iotMsgNotifyData.getBody() != null && + "ONLINE".equalsIgnoreCase(iotMsgNotifyData.getBody().getStatus())) { + onlineCountToAdd = 1; + log.debug("设备上线,增加在线计数: {}", deviceId); + } + + // 计算按摩时长 + if (iotMsgNotifyData.getBody() != null && iotMsgNotifyData.getBody().getServices() != null) { + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + if (props.containsKey("massage_start_time") && props.containsKey("massage_end_time")) { + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + if (startTime != null && startTime != 0 && endTime != null && startTime < endTime) { + durationToAdd = (endTime - startTime) / 1000; // 秒 + log.debug("增加运行时长: {}秒,设备: {}", durationToAdd, deviceId); + } + break; + } + } + } + } + + // 使用MyBatis Plus的saveOrUpdate方法,它会自动处理插入或更新 + boolean success = deviceRuntimeStatsMapper.insertOrUpdateStats( + deviceId, today, durationToAdd, onlineCountToAdd.longValue(),onlineCountToAdd.longValue(),onlineCountToAdd,new Date(),new Date(), "系统自动创建"); + + if (!success) { + // 如果插入失败,回退到查询+更新策略 + handleStatsUpdateFallback(deviceId, today, durationToAdd, onlineCountToAdd); + } + + } catch (Exception e) { + log.error("更新设备运行统计表时发生异常,设备ID: {}", deviceId, e); + } + } + + /** + * 回退处理:查询后更新 + */ + private void handleStatsUpdateFallback(String deviceId, Date statDate, Long durationToAdd, Integer onlineCountToAdd) { + try { + DeviceRuntimeStats existingStats = deviceRuntimeStatsMapper.selectByDeviceAndDate(deviceId, statDate); + if (existingStats == null) { + // 插入新记录 + DeviceRuntimeStats newStats = new DeviceRuntimeStats(); + newStats.setDeviceId(deviceId); + newStats.setStatDate(statDate); + newStats.setDailyDuration(durationToAdd); + newStats.setWeeklyDuration(durationToAdd); + newStats.setMonthlyDuration(durationToAdd); + newStats.setOnlineCount(onlineCountToAdd); + newStats.setCreateTime(new Date()); + newStats.setUpdateTime(new Date()); + newStats.setCreateBy("系统自动创建"); + deviceRuntimeStatsMapper.insert(newStats); + } else { + // 更新现有记录 + existingStats.setDailyDuration(existingStats.getDailyDuration() + durationToAdd); + existingStats.setWeeklyDuration(existingStats.getWeeklyDuration() + durationToAdd); + existingStats.setMonthlyDuration(existingStats.getMonthlyDuration() + durationToAdd); + existingStats.setOnlineCount(existingStats.getOnlineCount() + onlineCountToAdd); + existingStats.setUpdateTime(new Date()); + deviceRuntimeStatsMapper.updateById(existingStats); + } + } catch (Exception e) { + log.error("回退处理运行统计更新失败,设备ID: {}", deviceId, e); + } + } + + // ========== 安全的辅助方法 ========== + + /** + * 安全获取字符串值(修复空指针问题) + */ + private String safeGetString(Map props, String key) { + if (props == null || !props.containsKey(key)) { + return null; + } + Object value = props.get(key); + return value != null ? value.toString() : null; + } + + /** + * 安全解析时间戳 + */ + private Long safeParseTimestamp(Object timestampObj) { + if (timestampObj == null) return null; + + try { + if (timestampObj instanceof Number) { + long timestamp = ((Number) timestampObj).longValue(); + // 判断是秒级还是毫秒级时间戳 + if (timestamp < 10000000000L) { // 秒级时间戳 + return timestamp * 1000; + } else { // 毫秒级时间戳 + return timestamp; + } + } else if (timestampObj instanceof String) { + String timestampStr = timestampObj.toString().trim(); + if (timestampStr.isEmpty()) return null; + + if (timestampStr.contains(".")) { + // 处理浮点数时间戳 + double timestamp = Double.parseDouble(timestampStr); + return (long) (timestamp * 1000); + } else { + long timestamp = Long.parseLong(timestampStr); + if (timestamp < 10000000000L) { + return timestamp * 1000; + } else { + return timestamp; + } + } + } + } catch (Exception e) { + log.warn("时间戳解析失败: {}", timestampObj, e); + } + return null; + } + + /** + * 处理机器人信息 + */ + private void processRobotInfo(Device device, Map props) { + if (props == null) return; + + device.setSerialNumber(safeGetString(props, "serial_number")); + device.setSoftwareVersion(safeGetString(props, "software_version")); + device.setVtxdbVersion(safeGetString(props, "vortxdb_version")); + + // 处理位置信息 + if (props.containsKey("device_location")) { + Object location = props.get("device_location"); + if (location instanceof Map) { + try { + @SuppressWarnings("unchecked") + Map locationMap = (Map) location; + String error = safeGetString(locationMap, "error"); + if (!"Failed to retrieve location".equals(error)) { + device.setLongitude(safeGetString(locationMap, "longitude")); + device.setLatitude(safeGetString(locationMap, "latitude")); + } + } catch (Exception e) { + log.warn("处理位置信息失败", e); + } + } + } + } + + /** + * 处理许可证信息 + */ + private void processLicenseInfo(Device device, Map props) { + if (props == null) return; + + if (props.containsKey("is_activated")) { + Object isActivated = props.get("is_activated"); + if (isActivated != null) { + boolean activated = Boolean.parseBoolean(isActivated.toString()); + device.setLicenseStatus(activated ? "ACTIVE" : "INACTIVE"); + } + } + device.setActivationCode(safeGetString(props, "activation_code")); + + // 激活时间处理 + if (props.containsKey("activated_at") && device.getActivationTime() == null) { + // 这里需要根据实际时间格式进行解析,暂时使用当前时间 + device.setActivationTime(new Date()); + } + } + + /** + * 处理状态变更信息 + */ + private void processStatusChangeInfo(Device device, Map props) { + if (props == null) return; + + // 只有属性存在且不为空时才更新 + String softwareVersion = safeGetString(props, "software_version"); + if (softwareVersion != null) { + device.setSoftwareVersion(softwareVersion); + } + + String vtxdbVersion = safeGetString(props, "vtxdb_version"); + if (vtxdbVersion != null) { + device.setVtxdbVersion(vtxdbVersion); + } + + String longitude = safeGetString(props, "longitude"); + if (longitude != null) { + device.setLongitude(longitude); + } + + String latitude = safeGetString(props, "latitude"); + if (latitude != null) { + device.setLatitude(latitude); + } + } + + /** + * 处理VortXDB信息 + */ + private void processVortXDBInfo(Device device, Map props) { + if (props == null) return; + // VortXDB配置信息处理,可以根据需要添加具体逻辑 + device.setDescription("VortXDB配置信息已更新"); + } + + /** + * 判断是否是完整的按摩任务数据 + */ + private boolean isMassageTaskData(Map props) { + return props.containsKey("massage_start_time") && + props.containsKey("massage_end_time") && + props.containsKey("head_type") && + props.containsKey("body_part"); + } + + + /** + * 验证按摩任务数据完整性 + */ + private boolean isValidMassageTaskData(String deviceId, Map props) { + try { + // 检查必要字段 + if (!props.containsKey("massage_start_time") || !props.containsKey("massage_end_time") || + !props.containsKey("head_type") || !props.containsKey("body_part")) { + log.debug("按摩任务数据字段缺失,设备: {}", deviceId); + return false; + } + + // 检查时间有效性 + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + + if (startTime == null || endTime == null) { + log.debug("按摩任务时间解析失败,设备: {}", deviceId); + return false; + } + + if (startTime >= endTime) { + log.debug("按摩任务开始时间大于等于结束时间,设备: {}", deviceId); + return false; + } + + // 检查时长合理性(最大2小时) + long duration = endTime - startTime; + if (duration > 2 * 60 * 60 * 1000) { // 2小时 + log.warn("按摩任务时长异常(超过2小时),设备: {},时长: {}毫秒", deviceId, duration); + return false; + } + + // 检查按摩头和部位 + String headType = safeGetString(props, "head_type"); + String bodyPart = safeGetString(props, "body_part"); + if (StrUtil.isBlank(headType) || StrUtil.isBlank(bodyPart)) { + log.debug("按摩头或部位为空,设备: {}", deviceId); + return false; + } + + return true; + + } catch (Exception e) { + log.error("验证按摩任务数据失败,设备: {}", deviceId, e); + return false; + } + } + + /** + * 验证按摩时长合理性 + */ + private boolean isValidMassageDuration(MassageTask task) { + if (task.getTaskTime() == null) return false; + + // 按摩时长应该在1秒到2小时之间 + long duration = task.getTaskTime(); + if (duration <= 0 || duration > 2 * 60 * 60) { // 2小时 + log.warn("按摩时长异常: {}秒,设备: {}", duration, task.getDeviceId()); + return false; + } + + return true; + } + + // 修改按头使用统计,增加duration验证 + private void updateHeadUsageTable(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + String headType = safeGetString(props, "head_type"); + // 严格验证数据完整性 + if (StrUtil.isBlank(headType) || !isValidMassageTaskData(deviceId, props)) { + return; + } + + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + if (startTime == null || endTime == null || startTime >= endTime) { + return; + } + + long duration = (endTime - startTime) / 1000; // 转换为秒 + + // 验证duration合理性 + if (duration <= 0 || duration > 2 * 60 * 60) { // 最大2小时 + log.warn("按头使用统计时长异常: {}秒,设备: {},按摩头: {}", duration, deviceId, headType); + return; + } + + DeviceHeadUsage usage = deviceHeadUsageMapper.selectOne( + new LambdaQueryWrapper() + .eq(DeviceHeadUsage::getDeviceId, deviceId) + .eq(DeviceHeadUsage::getHeadType, headType)); + + if (usage == null) { + usage = new DeviceHeadUsage(); + usage.setDeviceId(deviceId); + usage.setHeadType(headType); + usage.setUsageCount(0); + usage.setTotalDuration(0L); + usage.setDailyDuration(0L); + usage.setWeeklyDuration(0L); + usage.setMonthlyDuration(0L); + usage.setCreateTime(new Date()); + } + + usage.setUpdateTime(new Date()); + usage.setUsageCount(usage.getUsageCount() + 1); + usage.setTotalDuration(usage.getTotalDuration() + duration); + usage.setDailyDuration(usage.getDailyDuration() + duration); + usage.setWeeklyDuration(usage.getWeeklyDuration() + duration); + usage.setMonthlyDuration(usage.getMonthlyDuration() + duration); + usage.setLastUsedTime(new Date()); + + if (usage.getId() == null) { + deviceHeadUsageMapper.insert(usage); + } else { + deviceHeadUsageMapper.updateById(usage); + } + + log.debug("按头使用统计更新: 设备{},按摩头{},时长{}秒", deviceId, headType, duration); + break; + } + } + } catch (Exception e) { + log.error("更新按头使用统计表时发生异常,设备ID: {}", deviceId, e); + } + } + + /** + * 处理服务数据中的信息,设置所有字段 + */ + private void processServiceDataForStatusLog(DeviceStatusLog statusLog, IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return; + } + + boolean hasMassageData = false; + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map properties = service.getProperties(); + // 检查是否有按摩数据 + if (properties.containsKey("massage_start_time") || + properties.containsKey("head_type") || + properties.containsKey("body_part")) { + + + hasMassageData = true; + + // 设置按摩头类型 + if (properties.containsKey("head_type")) { + Object headTypeObj = properties.get("head_type"); + if (headTypeObj != null) { + statusLog.setHeadType(headTypeObj.toString()); + } + } + + // 设置身体部位 + if (properties.containsKey("body_part")) { + Object bodyPartObj = properties.get("body_part"); + System.out.println("我是"+bodyPartObj); + if (bodyPartObj != null) { + statusLog.setBodyPart(bodyPartObj.toString()); + } + } + + // 设置按摩方案 + if (properties.containsKey("massage_plan")) { + Object massagePlanObj = properties.get("massage_plan"); + if (massagePlanObj != null) { + statusLog.setMassagePlan(massagePlanObj.toString()); + } + } + + // 处理按摩开始时间 + if (properties.containsKey("massage_start_time")) { + Object startTimeObj = properties.get("massage_start_time"); + if (startTimeObj != null) { + Long startTime = parseTimestampToMillis(startTimeObj); + statusLog.setMassageStartTime(startTime); + } + } + + // 处理按摩结束时间 + if (properties.containsKey("massage_end_time")) { + Object endTimeObj = properties.get("massage_end_time"); + if (endTimeObj != null) { + Long endTime = parseTimestampToMillis(endTimeObj); + statusLog.setMassageEndTime(endTime); + } + } + + // 计算并设置按摩持续时间 + if (statusLog.getMassageStartTime() != null && statusLog.getMassageEndTime() != null) { + long massageDuration = statusLog.getMassageEndTime() - statusLog.getMassageStartTime(); + statusLog.setMassageDuration(massageDuration); + + // 同时设置duration字段(秒为单位) + statusLog.setDuration((int)(massageDuration / 1000)); + } else if (properties.containsKey("massage_duration")) { + // 如果有直接提供的时长 + Object durationObj = properties.get("massage_duration"); + if (durationObj != null) { + try { + long duration = Long.parseLong(durationObj.toString()); + statusLog.setMassageDuration(duration * 1000); // 转为毫秒 + statusLog.setDuration((int)duration); // 秒 + } catch (NumberFormatException e) { + log.warn("按摩时长解析失败: {}", durationObj); + } + } + } + + // 如果是按摩任务,更改事件类型 + statusLog.setEventType("MASSAGE_TASK"); + } + + break; // 只处理第一个StatusChange服务 + } + } + + // 如果没有按摩数据,确保duration字段有默认值 + if (!hasMassageData && statusLog.getDuration() == null) { + statusLog.setDuration(0); + } + } + + /** + * 确保所有字段都有值(避免数据库约束问题) + */ + private void ensureAllFieldsSet(DeviceStatusLog statusLog) { + // 设置默认值,避免数据库约束问题 + if (statusLog.getDuration() == null) { + statusLog.setDuration(0); + } + + // 其他字段如果没有值,保持为null是可以的,因为数据库允许null + } + + /** + * 解析时间戳为毫秒 + */ + private Long parseTimestampToMillis(Object timestampObj) { + if (timestampObj == null) return null; + + try { + if (timestampObj instanceof Number) { + long timestamp = ((Number) timestampObj).longValue(); + // 判断是秒级还是毫秒级时间戳 + if (timestamp < 10000000000L) { // 秒级时间戳 + return timestamp * 1000; + } else { // 毫秒级时间戳 + return timestamp; + } + } else if (timestampObj instanceof String) { + String timestampStr = timestampObj.toString().trim(); + if (timestampStr.isEmpty()) return null; + + if (timestampStr.contains(".")) { + // 处理浮点数时间戳 + double timestamp = Double.parseDouble(timestampStr); + return (long) (timestamp * 1000); + } else { + long timestamp = Long.parseLong(timestampStr); + if (timestamp < 10000000000L) { + return timestamp * 1000; + } else { + return timestamp; + } + } + } + } catch (Exception e) { + log.warn("时间戳解析失败: {}", timestampObj, e); + } + return null; + } + + /** + * 检查是否需要记录状态变化 + */ + private boolean shouldLogStatusChange(String deviceId, String newStatus) { + try { + // 查询最近的状态记录 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(DeviceStatusLog::getDeviceId, deviceId) + .orderByDesc(DeviceStatusLog::getEventTime) + .last("LIMIT 1"); + + DeviceStatusLog lastLog = deviceStatusLogMapper.selectOne(queryWrapper); + + if (lastLog == null) { + return true; // 第一条记录 + } + + // 如果状态相同,检查时间间隔(避免频繁记录) + if (lastLog.getStatus().equals(newStatus)) { + long timeDiff = System.currentTimeMillis() - lastLog.getCreateTime().getTime(); + if (timeDiff < 5 * 60 * 1000) { // 5分钟内相同状态不记录 + return false; + } + } + + return true; + } catch (Exception e) { + log.warn("检查状态变化失败", e); + return true; // 出错时保守记录 + } + } + + /** + * 统一创建设备记录(线程安全)- 修复版本 + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) // 使用新事务,避免外层事务影响 + public Device getOrCreateDevice(String originalDeviceId, Consumer initializer) { + String normalizedDeviceId = normalizeDeviceId(originalDeviceId); + + // 第一步:快速检查设备是否存在(使用Redis缓存) + if (isDeviceExistsInCache(normalizedDeviceId)) { + Device device = getDeviceFromDB(normalizedDeviceId); + if (device != null) { + return device; + } + } + + // 第二步:使用更严格的分布式锁 + String lockKey = DEVICE_LOCK_PREFIX + normalizedDeviceId; + boolean locked = false; + + try { + // 使用更可靠的锁机制 + locked = acquireLockWithRetry(lockKey, 5, 1000); // 重试5次,每次间隔1秒 + + if (!locked) { + log.warn("获取设备锁失败,尝试直接查询: {}", normalizedDeviceId); + Device device = getDeviceFromDBWithWait(normalizedDeviceId); + if (device != null) { + return device; + } + throw new RuntimeException("设备创建失败,无法获取锁: " + normalizedDeviceId); + } + + // 第三步:加锁后再次检查(使用更严格的查询) + Device deviceInLock = getDeviceFromDBStrict(normalizedDeviceId); + if (deviceInLock != null) { + log.debug("加锁后设备已存在,返回现有设备: {}", normalizedDeviceId); + // 更新缓存 + cacheDeviceExistence(normalizedDeviceId); + return deviceInLock; + } + + // 第四步:创建设备(使用INSERT IGNORE或ON DUPLICATE KEY UPDATE) + Device newDevice = createNewDevice(normalizedDeviceId, initializer); + + try { + int result = deviceMapper.insertWithIgnore(newDevice); + if (result > 0) { + log.info("创建新设备成功: {}", normalizedDeviceId); + cacheDeviceExistence(normalizedDeviceId); + return newDevice; + } else { + // 插入失败,可能是重复键,再次查询 + log.debug("设备插入返回0,可能是重复键,重新查询: {}", normalizedDeviceId); + Device existingDevice = getDeviceFromDBStrict(normalizedDeviceId); + if (existingDevice != null) { + cacheDeviceExistence(normalizedDeviceId); + return existingDevice; + } + throw new RuntimeException("设备创建失败,插入返回0且查询不到设备: " + normalizedDeviceId); + } + } catch (DuplicateKeyException e) { + log.debug("捕获到重复键异常,设备已存在: {}", normalizedDeviceId); + Device existingDevice = getDeviceFromDBStrict(normalizedDeviceId); + if (existingDevice != null) { + cacheDeviceExistence(normalizedDeviceId); + return existingDevice; + } + throw new RuntimeException("设备重复键异常但查询不到设备: " + normalizedDeviceId, e); + } + + } catch (Exception e) { + log.error("创建设备失败: {}", normalizedDeviceId, e); + throw new RuntimeException("设备创建失败: " + e.getMessage(), e); + } finally { + // 释放锁 + if (locked) { + releaseLock(lockKey); + } + } + } + + /** + * 更可靠的锁获取机制 + */ + private boolean acquireLockWithRetry(String lockKey, int maxRetries, long retryIntervalMs) { + for (int i = 0; i < maxRetries; i++) { + try { + Boolean acquired = redisTemplate.opsForValue().setIfAbsent( + lockKey, + "1", + Duration.ofSeconds(10) // 锁有效期10秒 + ); + + if (acquired != null && acquired) { + return true; + } + + if (i < maxRetries - 1) { + TimeUnit.MILLISECONDS.sleep(retryIntervalMs); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } catch (Exception e) { + log.warn("获取锁异常,重试: {}", i + 1, e); + } + } + return false; + } + + /** + * 释放锁 + */ + private void releaseLock(String lockKey) { + try { + redisTemplate.delete(lockKey); + log.debug("释放设备锁: {}", lockKey); + } catch (Exception e) { + log.warn("释放设备锁失败: {}", lockKey, e); + } + } + + /** + * 检查设备是否存在(缓存优化) + */ + private boolean isDeviceExistsInCache(String deviceId) { + try { + String cacheKey = DEVICE_EXIST_PREFIX + deviceId; + Boolean exists = redisTemplate.hasKey(cacheKey); + return exists != null && exists; + } catch (Exception e) { + log.warn("检查设备缓存存在性失败", e); + return false; // 缓存失败时直接查询数据库 + } + } + + /** + * 缓存设备存在信息 + */ + private void cacheDeviceExistence(String deviceId) { + try { + String cacheKey = DEVICE_EXIST_PREFIX + deviceId; + redisTemplate.opsForValue().set(cacheKey, "1", Duration.ofMinutes(30)); // 缓存30分钟 + } catch (Exception e) { + log.warn("缓存设备存在信息失败", e); + } + } + + /** + * 严格的设备查询(使用SELECT FOR UPDATE在事务中) + */ + private Device getDeviceFromDBStrict(String deviceId) { + try { + return deviceMapper.selectOneForUpdate(deviceId); + } catch (Exception e) { + log.warn("严格查询设备失败", e); + return getDeviceFromDB(deviceId); // 降级为普通查询 + } + } + + /** + * 带等待的设备查询 + */ + private Device getDeviceFromDBWithWait(String deviceId) { + // 短暂等待后查询,给其他事务提交的时间 + try { + TimeUnit.MILLISECONDS.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return getDeviceFromDB(deviceId); + } + + /** + * 普通设备查询 + */ + private Device getDeviceFromDB(String deviceId) { + return deviceMapper.selectOne( + new LambdaQueryWrapper().eq(Device::getDeviceId, deviceId)); + } + + /** + * 创建新设备对象 + */ + private Device createNewDevice(String deviceId, Consumer initializer) { + Device device = new Device(); + device.setDeviceId(deviceId); + device.setDeviceName(deviceId); + device.setCreateTime(new Date()); + device.setCreateBy("系统自动创建"); + device.setIsDeleted(0); + device.setStatus("OFFLINE"); + + // 初始化运行时长统计 + device.setTotalOnlineDuration(0L); + device.setDailyDuration(0L); + device.setWeeklyDuration(0L); + device.setMonthlyDuration(0L); + + // 应用自定义初始化逻辑 + if (initializer != null) { + initializer.accept(device); + } + + return device; + } + + /** + * 统一的设备ID格式化方法 + */ + public String normalizeDeviceId(String deviceId) { + if (StrUtil.isBlank(deviceId)) { + return deviceId; + } + return deviceId.replaceAll(".*_", ""); + } + + // 在 DeviceDataProcessor.java 中修改 updateDeviceTable 方法 + private void updateDeviceTable(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + // 使用 try-catch 包装,防止设备创建失败影响其他数据处理 + Device device = deviceManagerService.getOrCreateDevice(deviceId, (newDevice) -> { + initializeNewDevice(newDevice, deviceId, iotMsgNotifyData); + }); + + // 更新设备信息(如果设备存在) + updateDeviceInfo(device, iotMsgNotifyData); + deviceMapper.updateById(device); + + log.debug("设备信息更新完成: {}", deviceId); + + } catch (Exception e) { + // 设备创建/更新失败,记录日志但不要抛出异常,避免影响其他表的处理 + log.error("设备信息处理失败,设备ID: {},错误: {}", deviceId, e.getMessage()); + log.debug("设备处理详细异常:", e); + } + } + + /** + * 初始化新设备信息 + */ + private void initializeNewDevice(Device device, String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + // 设置产品信息 + if (iotMsgNotifyData.getHeader() != null) { + device.setProductId(iotMsgNotifyData.getHeader().getProductId()); + } + + // 设置初始状态 + if (iotMsgNotifyData.getBody() != null) { + device.setStatus(iotMsgNotifyData.getBody().getStatus()); + } + } + + private void updateDeviceStatusLogTable(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + // 只记录状态变化,不记录重复的状态 + if (iotMsgNotifyData.getBody() == null) { + return; + } + + String newStatus = iotMsgNotifyData.getBody().getStatus(); + if (StrUtil.isBlank(newStatus)) { + return; + } + + // 规范化状态 + String normalizedStatus = "ONLINE".equalsIgnoreCase(newStatus) ? "ONLINE" : "OFFLINE"; + + // 检查是否需要记录(状态发生变化) + if (!shouldLogStatusChange(deviceId, normalizedStatus)) { + log.debug("状态未变化,跳过记录: {} -> {}", deviceId, normalizedStatus); + return; + } + + DeviceStatusLog statusLog = new DeviceStatusLog(); + statusLog.setDeviceId(deviceId); + statusLog.setCreateTime(new Date()); + statusLog.setStatus(normalizedStatus); + statusLog.setEventType("STATUS_CHANGE"); + + // 设置事件时间 - 优先使用消息中的时间,没有则用当前时间 + if (iotMsgNotifyData.getBody().getStatusUpdateTime() != null) { + statusLog.setEventTime(iotMsgNotifyData.getBody().getStatusUpdateTime()); + } else { + statusLog.setEventTime(new Date()); + } + + // 修复:添加空值检查 + if (iotMsgNotifyData.getBody().getServices() != null) { + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + String headType = safeGetString(props, "head_type"); + // 严格验证数据完整性 + if (StrUtil.isBlank(headType) || !isValidMassageTaskData(deviceId, props)) { + continue; // 改为continue而不是return,避免影响其他服务处理 + } + + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + if (startTime == null || endTime == null || startTime >= endTime) { + continue; + } + + long duration = (endTime - startTime) / 1000; // 转换为秒 + + // 验证duration合理性 + if (duration <= 0 || duration > 2 * 60 * 60) { // 最大2小时 + log.warn("按头使用统计时长异常: {}秒,设备: {},按摩头: {}", duration, deviceId, headType); + continue; + } + statusLog.setMassageStartTime(startTime); + statusLog.setMassageEndTime(endTime); + statusLog.setDuration((int)duration); + statusLog.setMassageDuration(duration); + String massagePlan = (String)props.get("massage_plan"); + if(massagePlan != null){ + statusLog.setMassagePlan(massagePlan); + } + } + } + } + + // 设置最后在线时间(如果可用) + if (iotMsgNotifyData.getBody().getLastOnlineTime() != null) { + try { + statusLog.setLastOnlineTime(iotMsgNotifyData.getBody().getLastOnlineTime()); + } catch (Exception e) { + log.warn("设置最后在线时间失败", e); + statusLog.setLastOnlineTime(new Date()); + } + } + + // 处理服务数据,设置所有相关字段 + processServiceDataForStatusLog(statusLog, iotMsgNotifyData); + + // 确保所有字段都有值(即使是null) + ensureAllFieldsSet(statusLog); + + deviceStatusLogMapper.insert(statusLog); + log.info("设备状态日志记录: {} -> {}", deviceId, normalizedStatus); + + } catch (Exception e) { + log.error("更新设备状态日志表时发生异常,设备ID: {}", deviceId, e); + } + } + + /** + * 统一处理设备数据 - 调用协调器确保所有表同步 + */ + @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) + public void processDeviceData(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) { + log.warn("接收到的设备信息数据为空"); + return; + } + + String deviceId = iotMsgNotifyData.getHeader().getDeviceId(); + if (StrUtil.isBlank(deviceId)) { + log.warn("设备ID为空,跳过数据处理"); + return; + } + + try { + // 使用协调器统一处理所有表数据 + deviceDataCoordinator.coordinateDeviceData(iotMsgNotifyData); + + log.info("设备数据处理完成: {}", deviceId); + } catch (Exception e) { + log.error("处理设备数据时发生异常,设备ID: {}", deviceId, e); + throw new RuntimeException("设备数据处理失败", e); + } + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/provider/DeviceRuntimeStatsSqlProvider.java b/storm-device/src/main/java/com/storm/device/provider/DeviceRuntimeStatsSqlProvider.java new file mode 100644 index 0000000..472997c --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/provider/DeviceRuntimeStatsSqlProvider.java @@ -0,0 +1,26 @@ +package com.storm.device.provider; + +import com.storm.common.core.utils.StringUtils; +import com.storm.device.domain.po.DeviceRuntimeStats; +import org.apache.ibatis.jdbc.SQL; + +public class DeviceRuntimeStatsSqlProvider { + + public String selectDeviceRuntimeStatsList(DeviceRuntimeStats deviceRuntimeStats) { + return new SQL(){{ + SELECT("*"); + FROM("device_runtime_stats"); + if (StringUtils.isNotBlank(deviceRuntimeStats.getDeviceId())) { + WHERE("device_id = #{deviceId}"); + } + if (deviceRuntimeStats.getStatDate() != null) { + WHERE("stat_date = #{statDate}"); + } + if (deviceRuntimeStats.getDailyDuration() != null) { + WHERE("daily_duration = #{dailyDuration}"); + } + ORDER_BY("stat_date DESC"); + }}.toString(); + } +} + diff --git a/storm-device/src/main/java/com/storm/device/service/IDeviceHeadUsageService.java b/storm-device/src/main/java/com/storm/device/service/IDeviceHeadUsageService.java new file mode 100644 index 0000000..87d2357 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/IDeviceHeadUsageService.java @@ -0,0 +1,56 @@ +package com.storm.device.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.storm.device.domain.po.DeviceHeadUsage; + +import java.util.List; + +/** + * 设备按头使用统计Service接口 + * + * @author storm + * @date 2025-09-25 + */ +public interface IDeviceHeadUsageService extends IService +{ + /** + * 查询设备按头使用统计 + * + * @param id 设备按头使用统计主键 + * @return 设备按头使用统计 + */ + DeviceHeadUsage selectDeviceHeadUsageById(Long id); + + /** + * 查询设备按头使用统计列表 + * + * @param deviceHeadUsage 设备按头使用统计 + * @return 设备按头使用统计集合 + */ + List selectDeviceHeadUsageList(DeviceHeadUsage deviceHeadUsage); + + /** + * 新增设备按头使用统计 + * + * @param deviceHeadUsage 设备按头使用统计 + * @return 结果 + */ + int insertDeviceHeadUsage(DeviceHeadUsage deviceHeadUsage); + + /** + * 修改设备按头使用统计 + * + * @param deviceHeadUsage 设备按头使用统计 + * @return 结果 + */ + int updateDeviceHeadUsage(DeviceHeadUsage deviceHeadUsage); + + /** + * 批量删除设备按头使用统计 + * + * @param ids 需要删除的设备按头使用统计主键集合 + * @return 结果 + */ + int deleteDeviceHeadUsageByIds(Long[] ids); +} diff --git a/storm-device/src/main/java/com/storm/device/service/IDeviceRuntimeStatsService.java b/storm-device/src/main/java/com/storm/device/service/IDeviceRuntimeStatsService.java new file mode 100644 index 0000000..e6134a3 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/IDeviceRuntimeStatsService.java @@ -0,0 +1,55 @@ +package com.storm.device.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.storm.device.domain.po.DeviceRuntimeStats; +import java.util.List; + +/** + * 设备运行时长统计Service接口 + * + * @author storm + * @date 2025-09-25 + */ +public interface IDeviceRuntimeStatsService extends IService +{ + /** + * 查询设备运行时长统计 + * + * @param id 设备运行时长统计主键 + * @return 设备运行时长统计 + */ + DeviceRuntimeStats selectDeviceRuntimeStatsById(Long id); + + /** + * 查询设备运行时长统计列表 + * + * @param deviceRuntimeStats 设备运行时长统计 + * @return 设备运行时长统计集合 + */ + List selectDeviceRuntimeStatsList(DeviceRuntimeStats deviceRuntimeStats); + + /** + * 新增设备运行时长统计 + * + * @param deviceRuntimeStats 设备运行时长统计 + * @return 结果 + */ + int insertDeviceRuntimeStats(DeviceRuntimeStats deviceRuntimeStats); + + /** + * 修改设备运行时长统计 + * + * @param deviceRuntimeStats 设备运行时长统计 + * @return 结果 + */ + int updateDeviceRuntimeStats(DeviceRuntimeStats deviceRuntimeStats); + + /** + * 批量删除设备运行时长统计 + * + * @param ids 需要删除的设备运行时长统计主键集合 + * @return 结果 + */ + int deleteDeviceRuntimeStatsByIds(Long[] ids); +} diff --git a/storm-device/src/main/java/com/storm/device/service/IDeviceService.java b/storm-device/src/main/java/com/storm/device/service/IDeviceService.java new file mode 100644 index 0000000..9f5b9a5 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/IDeviceService.java @@ -0,0 +1,51 @@ +package com.storm.device.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import java.util.Map; +import com.storm.device.domain.po.Device; +import com.storm.device.domain.vo.DevicePointVO; +import com.storm.device.domain.vo.DeviceTotalVO; +import com.storm.device.domain.vo.DeviceVo; +import com.storm.device.task.vo.IotMsgNotifyDataPro; + +/** + * 设备基本信息Service接口 + * + * @author storm + * @date 2025-08-22 + */ +public interface IDeviceService extends IService +{ + + /** + * 导出设备基本信息列表 + * + * @param device 设备基本信息 + * @return 设备基本信息集合 + */ + public List exportDeviceList(Device device); + + /** + * 批量删除设备基本信息 + * + * @param ids 需要删除的设备基本信息主键集合 + * @return 结果 + */ + public int deleteDeviceByIds(Long[] ids); + + void syncDeviceList(); + + DeviceVo getInfo(String deviceId); + + List selectDeviceList(Device device); + + List> queryServiceProperties(String deviceId); + + DeviceTotalVO getTotal(); + + List getDeviceCoordinate(); + + void updateDeviceInfo(IotMsgNotifyDataPro iotMsgNotifyData); +} diff --git a/storm-device/src/main/java/com/storm/device/service/IDeviceShadowService.java b/storm-device/src/main/java/com/storm/device/service/IDeviceShadowService.java new file mode 100644 index 0000000..86dadba --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/IDeviceShadowService.java @@ -0,0 +1,42 @@ +package com.storm.device.service; + +import java.util.List; +import java.util.Map; + +public interface IDeviceShadowService { + + /** + * 获取设备影子数据 + */ + Map getDeviceShadowData(String deviceId); + + /** + * 获取设备运行统计 + */ + Map getRuntimeStats(String deviceId); + + /** + * 获取设备按摩统计 + */ + Map getMassageStats(String deviceId, String timeRange); + + /** + * 获取设备实时状态 + */ + Map getRealtimeStatus(String deviceId); + + /** + * 获取设备状态历史 + */ + List> getStatusHistory(String deviceId, int limit); + + /** + * 获取设备按摩历史 + */ + List> getMassageHistory(String deviceId, int limit); + + /** + * 获取设备健康状态 + */ + Map getHealthStatus(String deviceId); +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/service/IDeviceStatusLogService.java b/storm-device/src/main/java/com/storm/device/service/IDeviceStatusLogService.java new file mode 100644 index 0000000..5d7e51e --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/IDeviceStatusLogService.java @@ -0,0 +1,22 @@ +package com.storm.device.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.storm.device.domain.po.DeviceStatusLog; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import java.util.List; + +/** + * 设备状态日志Service接口 + * + * @author storm + * @date 2025-08-28 + */ +public interface IDeviceStatusLogService extends IService +{ + + List selectDeviceStatusLogList(DeviceStatusLog deviceStatusLog); + + void updateDeviceStatusLog(IotMsgNotifyDataPro iotMsgNotifyData); + boolean safeInsertStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData); +} diff --git a/storm-device/src/main/java/com/storm/device/service/IMassageTaskService.java b/storm-device/src/main/java/com/storm/device/service/IMassageTaskService.java new file mode 100644 index 0000000..f56a3d8 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/IMassageTaskService.java @@ -0,0 +1,42 @@ +package com.storm.device.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.device.domain.dto.MassageTaskStatDTO; +import com.storm.device.domain.po.MassageTask; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +/** + * 按摩任务记录Service接口 + * + * @author storm + * @date 2025-08-28 + */ +public interface IMassageTaskService extends IService { + + List> getHeadTypeStats(String startTime, String endTime); + + List> getBodyPartStats(String startTime, String endTime); + + List getMassageTaskStatsByRange(String startTime, String endTime); + + List> getDeviceMassageStats(String startTime, String endTime); + + LocalDate getMinDate(); + + List> getDailyStats(String startTime, String endTime); + + AjaxResult getTrendInsight(String startTime, String endTime); + + Map getStats(); + + Map getTotals(); + + List selectMassageTaskList(MassageTask massageTask); + + void updateMassageTask(IotMsgNotifyDataPro iotMsgNotifyData); +} diff --git a/storm-device/src/main/java/com/storm/device/service/impl/DeviceHeadUsageServiceImpl.java b/storm-device/src/main/java/com/storm/device/service/impl/DeviceHeadUsageServiceImpl.java new file mode 100644 index 0000000..63ec391 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/impl/DeviceHeadUsageServiceImpl.java @@ -0,0 +1,84 @@ +package com.storm.device.service.impl; + +import java.util.List; + +import com.storm.device.domain.po.DeviceHeadUsage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.device.mapper.DeviceHeadUsageMapper; +import com.storm.device.service.IDeviceHeadUsageService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.Arrays; + +/** + * 设备按头使用统计Service业务层处理 + * + * @author storm + * @date 2025-09-25 + */ +@Service +public class DeviceHeadUsageServiceImpl extends ServiceImpl implements IDeviceHeadUsageService +{ + @Autowired + private DeviceHeadUsageMapper deviceHeadUsageMapper; + + /** + * 查询设备按头使用统计 + * + * @param id 设备按头使用统计主键 + * @return 设备按头使用统计 + */ + @Override + public DeviceHeadUsage selectDeviceHeadUsageById(Long id) + { + return getById(id); + } + + /** + * 查询设备按头使用统计列表 + * + * @param deviceHeadUsage 设备按头使用统计 + * @return 设备按头使用统计 + */ + @Override + public List selectDeviceHeadUsageList(DeviceHeadUsage deviceHeadUsage) + { + return deviceHeadUsageMapper.selectDeviceHeadUsageList(deviceHeadUsage); + } + + /** + * 新增设备按头使用统计 + * + * @param deviceHeadUsage 设备按头使用统计 + * @return 结果 + */ + @Override + public int insertDeviceHeadUsage(DeviceHeadUsage deviceHeadUsage) + { + return save(deviceHeadUsage) ? 1 : 0; + } + + /** + * 修改设备按头使用统计 + * + * @param deviceHeadUsage 设备按头使用统计 + * @return 结果 + */ + @Override + public int updateDeviceHeadUsage(DeviceHeadUsage deviceHeadUsage) + { + return updateById(deviceHeadUsage) ? 1 : 0; + } + + /** + * 批量删除设备按头使用统计 + * + * @param ids 需要删除的设备按头使用统计主键 + * @return 结果 + */ + @Override + public int deleteDeviceHeadUsageByIds(Long[] ids) + { + return removeByIds(Arrays.asList(ids)) ? 1 : 0; + } +} diff --git a/storm-device/src/main/java/com/storm/device/service/impl/DeviceRuntimeStatsServiceImpl.java b/storm-device/src/main/java/com/storm/device/service/impl/DeviceRuntimeStatsServiceImpl.java new file mode 100644 index 0000000..6c0db31 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/impl/DeviceRuntimeStatsServiceImpl.java @@ -0,0 +1,258 @@ +package com.storm.device.service.impl; + +import java.util.Date; +import java.util.List; + +import com.storm.device.domain.po.DeviceRuntimeStats; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import com.storm.device.mapper.DeviceRuntimeStatsMapper; +import com.storm.device.service.IDeviceRuntimeStatsService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import java.util.Arrays; +import java.util.Map; + +/** + * 设备运行时长统计Service业务层处理 + * + * @author storm + * @date 2025-09-25 + */ +@Service +@Slf4j +public class DeviceRuntimeStatsServiceImpl extends ServiceImpl implements IDeviceRuntimeStatsService +{ + @Autowired + private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper; + + /** + * 查询设备运行时长统计 + * + * @param id 设备运行时长统计主键 + * @return 设备运行时长统计 + */ + @Override + public DeviceRuntimeStats selectDeviceRuntimeStatsById(Long id) + { + return getById(id); + } + + /** + * 查询设备运行时长统计列表 + * + * @param deviceRuntimeStats 设备运行时长统计 + * @return 设备运行时长统计 + */ + @Override + public List selectDeviceRuntimeStatsList(DeviceRuntimeStats deviceRuntimeStats) + { + return deviceRuntimeStatsMapper.selectDeviceRuntimeStatsList(deviceRuntimeStats); + } + + /** + * 新增设备运行时长统计 + * + * @param deviceRuntimeStats 设备运行时长统计 + * @return 结果 + */ + @Override + public int insertDeviceRuntimeStats(DeviceRuntimeStats deviceRuntimeStats) + { + return save(deviceRuntimeStats) ? 1 : 0; + } + + /** + * 修改设备运行时长统计 + * + * @param deviceRuntimeStats 设备运行时长统计 + * @return 结果 + */ + @Override + public int updateDeviceRuntimeStats(DeviceRuntimeStats deviceRuntimeStats) + { + return updateById(deviceRuntimeStats) ? 1 : 0; + } + + /** + * 批量删除设备运行时长统计 + * + * @param ids 需要删除的设备运行时长统计主键 + * @return 结果 + */ + @Override + public int deleteDeviceRuntimeStatsByIds(Long[] ids) + { + return removeByIds(Arrays.asList(ids)) ? 1 : 0; + } + + /** + * 安全更新运行统计 - 修复重复累加问题 + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public boolean safeUpdateRuntimeStats(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + Date today = new Date(); + Date now = new Date(); + + // 计算要增加的时长和在线次数 + Long durationToAdd = calculateMassageDuration(iotMsgNotifyData); + Integer onlineCountToAdd = shouldCountOnline(iotMsgNotifyData) ? 1 : 0; + + // 先尝试查询今日是否已有记录 + DeviceRuntimeStats existingStats = deviceRuntimeStatsMapper.selectTodayStats(deviceId); + + if (existingStats == null) { + // 插入新记录 + DeviceRuntimeStats newStats = new DeviceRuntimeStats(); + newStats.setDeviceId(deviceId); + newStats.setStatDate(today); + newStats.setDailyDuration(durationToAdd); + newStats.setWeeklyDuration(durationToAdd); + newStats.setMonthlyDuration(durationToAdd); + newStats.setOnlineCount(onlineCountToAdd); + newStats.setCreateTime(now); + newStats.setUpdateTime(now); + newStats.setCreateBy("系统自动创建"); + + deviceRuntimeStatsMapper.insert(newStats); + log.debug("创建新的运行统计记录: {}", deviceId); + } else { + // 更新现有记录 - 确保不重复累加 + long newDailyDuration = existingStats.getDailyDuration() + durationToAdd; + long newWeeklyDuration = existingStats.getWeeklyDuration() + durationToAdd; + long newMonthlyDuration = existingStats.getMonthlyDuration() + durationToAdd; + int newOnlineCount = existingStats.getOnlineCount() + onlineCountToAdd; + + // 验证数据合理性 + if (isDurationReasonable(newDailyDuration) && + isDurationReasonable(newWeeklyDuration) && + isDurationReasonable(newMonthlyDuration)) { + + existingStats.setDailyDuration(newDailyDuration); + existingStats.setWeeklyDuration(newWeeklyDuration); + existingStats.setMonthlyDuration(newMonthlyDuration); + existingStats.setOnlineCount(newOnlineCount); + existingStats.setUpdateTime(now); + + deviceRuntimeStatsMapper.updateById(existingStats); + log.debug("更新运行统计记录: {} -> 增加{}秒", deviceId, durationToAdd); + } else { + log.warn("运行统计时长异常,跳过更新: {}", deviceId); + return false; + } + } + return true; + + } catch (DuplicateKeyException e) { + log.debug("运行统计记录已存在(并发情况),设备: {}", deviceId); + // 递归重试一次 + return safeUpdateRuntimeStatsRetry(deviceId, iotMsgNotifyData); + } catch (Exception e) { + log.error("更新运行统计失败,设备: {}", deviceId, e); + return false; + } + } + + /** + * 重试机制 + */ + private boolean safeUpdateRuntimeStatsRetry(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + Thread.sleep(100); // 短暂等待 + return safeUpdateRuntimeStats(deviceId, iotMsgNotifyData); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return false; + } catch (Exception e) { + log.error("运行统计重试也失败,设备: {}", deviceId, e); + return false; + } + } + + /** + * 计算按摩时长 + */ + private Long calculateMassageDuration(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return 0L; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + + if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { + long durationMs = endTime - startTime; + long durationSeconds = durationMs / 1000; + + // 验证时长合理性 + if (isDurationReasonable(durationSeconds)) { + return durationSeconds; + } else { + log.warn("按摩时长异常: {}秒,设备: {}", durationSeconds, + iotMsgNotifyData.getHeader().getDeviceId()); + return 0L; + } + } + break; + } + } + return 0L; + } + + /** + * 判断是否应该计数为上线 + */ + private boolean shouldCountOnline(IotMsgNotifyDataPro iotMsgNotifyData) { + return iotMsgNotifyData.getBody() != null && + "ONLINE".equalsIgnoreCase(iotMsgNotifyData.getBody().getStatus()); + } + + /** + * 验证时长是否合理(最大24小时) + */ + private boolean isDurationReasonable(long durationSeconds) { + return durationSeconds >= 0 && durationSeconds <= 24 * 60 * 60; // 24小时 + } + + /** + * 安全解析时间戳 + */ + private Long safeParseTimestamp(Object timestampObj) { + if (timestampObj == null) return null; + + try { + if (timestampObj instanceof Number) { + long timestamp = ((Number) timestampObj).longValue(); + if (timestamp < 10000000000L) { // 秒级时间戳 + return timestamp * 1000; + } else { // 毫秒级时间戳 + return timestamp; + } + } else if (timestampObj instanceof String) { + String timestampStr = timestampObj.toString(); + if (timestampStr.contains(".")) { + double timestamp = Double.parseDouble(timestampStr); + return (long) (timestamp * 1000); + } else { + long timestamp = Long.parseLong(timestampStr); + if (timestamp < 10000000000L) { + return timestamp * 1000; + } else { + return timestamp; + } + } + } + } catch (Exception e) { + log.warn("时间戳解析失败: {}", timestampObj); + } + return null; + } +} diff --git a/storm-device/src/main/java/com/storm/device/service/impl/DeviceServiceImpl.java b/storm-device/src/main/java/com/storm/device/service/impl/DeviceServiceImpl.java new file mode 100644 index 0000000..c33e88f --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/impl/DeviceServiceImpl.java @@ -0,0 +1,709 @@ +package com.storm.device.service.impl; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.*; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.huaweicloud.sdk.iotda.v5.IoTDAClient; +import com.huaweicloud.sdk.iotda.v5.model.*; +import com.storm.common.core.exception.base.BaseException; +import com.storm.common.core.utils.BeanUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.device.domain.po.DeviceStatusLog; +import com.storm.device.domain.vo.DevicePointVO; +import com.storm.device.domain.vo.DeviceTotalVO; +import com.storm.device.domain.vo.DeviceVo; +import com.storm.device.manager.DeviceManagerService; +import com.storm.device.mapper.DeviceStatusLogMapper; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.device.mapper.DeviceMapper; +import com.storm.device.domain.po.Device; +import com.storm.device.service.IDeviceService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.transaction.annotation.Transactional; +import javax.annotation.Resource; +import java.util.stream.Collectors; + +/** + * 设备基本信息Service业务层处理 + * + * @author storm + * @date 2025-08-22 + */ +@Slf4j +@Service +public class DeviceServiceImpl extends ServiceImpl implements IDeviceService { + @Autowired + private DeviceMapper deviceMapper; + + @Resource + private IoTDAClient ioTDAClient; + + @Resource + private DeviceStatusLogMapper deviceStatusLogMapper; + + @Autowired + private DeviceManagerService deviceManagerService; + + private final String apiKey = ""; // 替换为你的百度地图 API Key + + private final OkHttpClient okHttpClient = new OkHttpClient(); + + /** + * 获取设备总数及在线离线设备数量 + * + * @return + */ + @Override + public DeviceTotalVO getTotal() { + DeviceTotalVO deviceTotalVO = new DeviceTotalVO(); + //查询设备总数量 + Long totalCount = deviceMapper.selectCount(Wrappers.lambdaQuery(Device.class) + .eq(Device::getIsDeleted, 0)); + deviceTotalVO.setTotal(totalCount.toString()); + //查询设备在线数量 + Long onlineCount = deviceMapper.selectCount(Wrappers.lambdaQuery(Device.class) + .eq(Device::getStatus, "ONLINE") + .eq(Device::getIsDeleted, 0)); + + // 计算离线数量 + Long offlineCount = totalCount - onlineCount; + + // 创建在线设备数据项 + DeviceTotalVO.DeviceStatusData onlineData = new DeviceTotalVO.DeviceStatusData(); + onlineData.setValue(onlineCount.intValue()); + onlineData.setName("在线设备"); + + // 创建离线设备数据项 + DeviceTotalVO.DeviceStatusData offlineData = new DeviceTotalVO.DeviceStatusData(); + offlineData.setValue(offlineCount.intValue()); + offlineData.setName("离线设备"); + + // 设置数据列表 + deviceTotalVO.setData(Arrays.asList(onlineData, offlineData)); + return deviceTotalVO; + } + + /** + * 获取设备坐标 + * @return + */ + @Override + public List getDeviceCoordinate() { + // 查询所有设备的经纬度 + List devices = this.lambdaQuery() + .select(Device::getDeviceId, Device::getDeviceName, Device::getLongitude, Device::getLatitude) + .list(); + // 查出来的devices里面设备的经纬度可能不存在,如果不存在就不封装 + List devicePointVOS = devices.stream() + .map(device -> { + Double lng = safeParseDouble(device.getLongitude()); + Double lat = safeParseDouble(device.getLatitude()); + + if (lng == null || lat == null) { + log.debug("设备坐标为空或格式错误,设备ID: {}, 经度: {}, 纬度: {}", + device.getDeviceId(), device.getLongitude(), device.getLatitude()); + return null; + } + + DevicePointVO devicePointVO = new DevicePointVO(); + devicePointVO.setName(device.getDeviceName()); + devicePointVO.setLng(lng); + devicePointVO.setLat(lat); + + return devicePointVO; + }) + .filter(Objects::nonNull) // 过滤掉无效的对象 + .collect(Collectors.toList()); + log.info("设备坐标获取成功,数量: {}", devicePointVOS); + + return devicePointVOS; + } + + // 添加一个安全的Double解析方法 + private Double safeParseDouble(String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + try { + return Double.parseDouble(value.trim()); + } catch (NumberFormatException e) { + return null; + } + } + + // 百度地图API获取地址 + public String getLocationFromCoordinates(double latitude, double longitude) { + String url = String.format( + "https://api.map.baidu.com/reverse_geocoding/v3/?ak=%s&extensions_poi=1&entire_poi=1&sort_strategy=distance&output=json&coordtype=bd09ll&location=%f,%f", + apiKey, latitude, longitude); + + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + if (!response.isSuccessful()) throw new IOException("Unexpected response " + response); + + String responseBody = response.body().string(); + if (responseBody.isEmpty()) { + return "Empty response"; + } + + // fastjson 解析 + com.alibaba.fastjson.JSONObject jsonResponse = com.alibaba.fastjson.JSONObject.parseObject(responseBody); + + if (jsonResponse.getInteger("status") == 0) { + com.alibaba.fastjson.JSONObject result = jsonResponse.getJSONObject("result"); + return result.getString("formatted_address_poi"); + } else { + return "Unknown location"; + } + } catch (IOException e) { + e.printStackTrace(); + return "Error fetching location"; + } + } + + public LocalDateTime otherLocalDateTime(String activeTime) { + LocalDateTime time = LocalDateTimeUtil.parse(activeTime, "yyyyMMdd'T'HHmmss'Z'"); + time = time.atZone(ZoneId.from(ZoneOffset.UTC)) + .withZoneSameInstant(ZoneId.of("Asia/Shanghai")) + .toLocalDateTime(); + return time; + } + + // 在 DeviceServiceImpl 中添加设备使用时间统计方法 + /** + * 更新设备使用时间统计 + */ + public void updateDeviceUsageStats(String deviceId, Long duration) { + if (duration == null || duration <= 0) { + return; + } + + try { + Device device = this.getOne(new LambdaQueryWrapper().eq(Device::getDeviceId, deviceId)); + if (device == null) { + return; + } + + // 更新总使用时长 + device.setTotalOnlineDuration(device.getTotalOnlineDuration() + duration); + + // 更新今日使用时长(需要判断是否是同一天) + Date now = new Date(); + if (isSameDay(device.getUpdateTime(), now)) { + device.setDailyDuration(device.getDailyDuration() + duration); + } else { + device.setDailyDuration(duration); + } + + // 更新本周、本月时长(需要更复杂的逻辑,这里简化处理) + device.setWeeklyDuration(device.getWeeklyDuration() + duration); + device.setMonthlyDuration(device.getMonthlyDuration() + duration); + + device.setUpdateTime(now); + this.updateById(device); + + } catch (Exception e) { + log.error("更新设备使用时间统计失败: {}", deviceId, e); + } + } + + private boolean isSameDay(Date date1, Date date2) { + if (date1 == null || date2 == null) return false; + Calendar cal1 = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal1.setTime(date1); + cal2.setTime(date2); + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && + cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR); + } + + /** + * 处理状态变更 - 修复空指针问题 + */ + private void processStatusChange(Device device, Map properties, IotMsgNotifyDataPro iotMsgNotifyData) { + if (properties == null) { + log.warn("properties为空,跳过状态变更处理"); + return; + } + + try { + // 更新设备状态 - 安全处理 + if (properties.containsKey("status") && properties.get("status") != null) { + String status = properties.get("status").toString(); + device.setStatus("ONLINE".equalsIgnoreCase(status) ? "ONLINE" : "OFFLINE"); + + Date now = new Date(); + if ("ONLINE".equalsIgnoreCase(status)) { + device.setLastOnlineTime(now); + } else { + device.setLastOfflineTime(now); + } + } + + // 处理位置信息 - 安全处理 + if (properties.containsKey("longitude") && properties.get("longitude") != null) { + device.setLongitude(properties.get("longitude").toString()); + } + if (properties.containsKey("latitude") && properties.get("latitude") != null) { + device.setLatitude(properties.get("latitude").toString()); + } + + // 处理版本信息 - 安全处理 + if (properties.containsKey("software_version") && properties.get("software_version") != null) { + device.setSoftwareVersion(properties.get("software_version").toString()); + } + if (properties.containsKey("vtxdb_version") && properties.get("vtxdb_version") != null) { + device.setVtxdbVersion(properties.get("vtxdb_version").toString()); + } + + } catch (Exception e) { + log.error("处理状态变更数据异常", e); + } + } + + /** + * 导出设备基本信息列表 + * + * @param device 设备基本信息 + * @return 设备基本信息 + */ + @Override + public List exportDeviceList(Device device) { + return deviceMapper.selectDeviceList(device); + } + + /** + * 批量删除设备基本信息 + * + * @param ids 需要删除的设备基本信息主键 + * @return 结果 + */ + @Override + public int deleteDeviceByIds(Long[] ids) { + // 修改设备状态isDeleted为1 + return update(Wrappers.lambdaUpdate() + .set(Device::getIsDeleted, 1) + .in(Device::getId, ids)) ? 1 : 0; + } + + /** + * 从物联网平台同步设备列表 + * + * @return + */ + @Override + public void syncDeviceList() { + // 创建请求对象 + ListDevicesRequest request = new ListDevicesRequest(); + request.setLimit(50); // 每页最大50条 + + try { + // 发起首次请求 + ListDevicesResponse response = ioTDAClient.listDevices(request); + + // 处理返回的设备数据 + List iotResultList = new ArrayList<>(response.getDevices()); + + while (CollectionUtils.isNotEmpty(response.getDevices())) { + + // 设置分页参数 + request.setMarker(response.getPage().getMarker()); + + // 发起请求获取设备列表 + response = ioTDAClient.listDevices(request); + + if (response.getHttpStatusCode() != 200) { + throw new BaseException("物联网接口 - 查询设备列表失败,HTTP状态码: " + response.getHttpStatusCode()); + } + + // 处理返回的设备数据 + iotResultList.addAll(response.getDevices()); + + // 休眠100毫秒 + Thread.sleep(100); + } + + // 修复:将 QueryDeviceSimplify 转换为设备信息并保存 + syncDeviceInfoFromPlatform(iotResultList); + + } catch (Exception e) { + log.error("同步设备列表失败: {}", e.getMessage(), e); + throw new BaseException("同步设备列表失败: " + e.getMessage()); + } + } + + /** + * 从平台同步的设备信息保存到数据库 + */ + private void syncDeviceInfoFromPlatform(List deviceList) { + if (CollectionUtils.isEmpty(deviceList)) { + log.info("没有需要同步的设备数据"); + return; + } + + log.info("开始同步 {} 个设备信息", deviceList.size()); + + for (QueryDeviceSimplify platformDevice : deviceList) { + try { + String originalDeviceId = platformDevice.getDeviceId(); + if (StrUtil.isBlank(originalDeviceId)) { + log.warn("设备ID为空,跳过处理"); + continue; + } + + // 使用统一的设备ID格式 + String normalizedDeviceId = normalizeDeviceId(originalDeviceId); + + // 查询设备是否存在 + Device device = this.getOne(new LambdaQueryWrapper().eq(Device::getDeviceId, normalizedDeviceId)); + if (device == null) { + // 创建新设备 + device = new Device(); + device.setDeviceId(normalizedDeviceId); + device.setDeviceName(platformDevice.getDeviceName() != null ? + platformDevice.getDeviceName() : normalizedDeviceId); + device.setProductId(platformDevice.getProductId()); + device.setStatus(platformDevice.getStatus() != null ? + platformDevice.getStatus() : "OFFLINE"); + device.setCreateTime(new Date()); + device.setCreateBy("系统自动创建"); + device.setDescription("从物联网平台同步的设备"); + + log.info("创建新设备: {}", normalizedDeviceId); + } else { + // 更新现有设备 + device.setDeviceName(platformDevice.getDeviceName() != null ? + platformDevice.getDeviceName() : device.getDeviceName()); + device.setStatus(platformDevice.getStatus() != null ? + platformDevice.getStatus() : device.getStatus()); + device.setUpdateTime(new Date()); + device.setUpdateBy("系统自动创建"); + + log.info("更新设备信息: {}", normalizedDeviceId); + } + + // 保存设备信息 + boolean saveResult = this.saveOrUpdate(device); + if (saveResult) { + log.debug("设备信息同步成功: {}", normalizedDeviceId); + } else { + log.error("设备信息保存失败: {}", normalizedDeviceId); + } + + } catch (Exception e) { + log.error("同步设备信息时发生异常,deviceId: {}", platformDevice.getDeviceId(), e); + } + } + + log.info("设备信息同步完成,共处理 {} 个设备", deviceList.size()); + } + + /** + * 获取设备详细信息 + * + * @param deviceId + * @return + */ + @Override + public DeviceVo getInfo(String deviceId) { + // 使用统一的设备ID格式 + String normalizedDeviceId = normalizeDeviceId(deviceId); + + // 查询设备表 + Device device = deviceMapper.selectOne(Wrappers.lambdaQuery(Device.class) + .eq(Device::getDeviceId, normalizedDeviceId)); + if (device == null) { + return null; + } + // 查询设备状态日志表 + DeviceStatusLog deviceStatusLog = deviceStatusLogMapper.selectById(normalizedDeviceId); + + // 封装VO返回 + DeviceVo deviceVo = BeanUtils.toBean(device, DeviceVo.class); + if (deviceStatusLog != null) { + deviceVo.setLastOnlineTime(deviceStatusLog.getLastOnlineTime()); + } else { + deviceVo.setLastOnlineTime(null); + } + + return deviceVo; + } + + /** + * 查询设备列表 + * + * @param device 查询条件 + * @return 设备列表 + */ + @Override + public List selectDeviceList(Device device) { + // 如果查询条件中有设备ID,先进行归一化处理 + if (StringUtils.isNotBlank(device.getDeviceId())) { + device.setDeviceId(normalizeDeviceId(device.getDeviceId())); + } + + // 构建查询条件 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(StringUtils.isNotBlank(device.getDeviceId()), Device::getDeviceId, device.getDeviceId()) + .like(StringUtils.isNotBlank(device.getDeviceName()), Device::getDeviceName, device.getDeviceName()) + .eq(StringUtils.isNotBlank(device.getProductId()), Device::getProductId, device.getProductId()) + .like(StringUtils.isNotBlank(device.getProductName()), Device::getProductName, device.getProductName()) + .eq(StringUtils.isNotBlank(device.getStatus()), Device::getStatus, device.getStatus()) + .eq(device.getDeviceType() != null, Device::getDeviceType, device.getDeviceType()) + .like(StringUtils.isNotBlank(device.getLocation()), Device::getLocation, device.getLocation()) + .eq(StringUtils.isNotBlank(device.getShopId()), Device::getShopId, device.getShopId()) + .eq(StringUtils.isNotBlank(device.getLongitude()), Device::getLongitude, device.getLongitude()) + .eq(StringUtils.isNotBlank(device.getLatitude()), Device::getLatitude, device.getLatitude()) + .eq(StringUtils.isNotBlank(device.getSoftwareVersion()), Device::getSoftwareVersion, device.getSoftwareVersion()) + .eq(StringUtils.isNotBlank(device.getVtxdbVersion()), Device::getVtxdbVersion, device.getVtxdbVersion()) + .eq(StringUtils.isNotBlank(device.getCreateBy()), Device::getCreateBy, device.getCreateBy()) + .eq(StringUtils.isNotBlank(device.getUpdateBy()), Device::getUpdateBy, device.getUpdateBy()) + .eq(device.getIsDeleted() != null, Device::getIsDeleted, device.getIsDeleted()) + .eq(Device::getIsDeleted, 0); // 默认只查询未删除的设备 + + // 若依框架会自动处理分页,这里直接返回查询结果 + List devices = deviceMapper.selectList(wrapper); + return devices; + } + + /** + * 查询设备上报数据 + * + * @param deviceId + * @return + */ + @Override + public List> queryServiceProperties(String deviceId) { + // 使用统一的设备ID格式 + String normalizedDeviceId = normalizeDeviceId(deviceId); + + //通过client 调用查询设备影子数据 + ShowDeviceShadowRequest request = new ShowDeviceShadowRequest(); + request.setDeviceId(normalizedDeviceId); + ShowDeviceShadowResponse response = ioTDAClient.showDeviceShadow(request); + + //解析数据 + List shadow = response.getShadow(); + if (CollUtil.isEmpty(shadow)) { + log.error("设备影子数据为空"); + return new ArrayList<>(); + } + JSONObject jsonObject = new JSONObject(); + for (DeviceShadowData deviceShadowData : shadow) { + if ("StatusChange".equals(deviceShadowData.getServiceId())) { + jsonObject = JSONUtil.parseObj(deviceShadowData.getReported().getProperties()); + } + } + + String eventTimeStr = shadow.get(1).getReported().getEventTime(); + LocalDateTime time = otherLocalDateTime(eventTimeStr); + + //定义返回的结果 + List> list = new ArrayList<>(); + jsonObject.forEach((key, value) -> { + Map map = new HashMap<>(); + // 如果key是online_time或massage_time则用otherLocalDateTime转换 + if ("massage_start_time".equals(key) || "massage_end_time".equals(key)) { + value = otherLocalDateTime(value.toString()); + } + map.put("functionId", key); + map.put("value", value); + map.put("eventTime", time); + list.add(map); + }); + return list; + } + + // TODO + /** + * 统一的设备ID格式化方法 + */ + private String normalizeDeviceId(String deviceId) { + if (StrUtil.isBlank(deviceId)) { + return deviceId; + } + return deviceId.replaceAll(".*_", ""); + } + + /** + * 毫秒时间戳转换 + */ + private Date parseMillisecondTimestamp(Object timestampObj) { + if (timestampObj == null) return null; + + try { + if (timestampObj instanceof Number) { + long timestamp = ((Number) timestampObj).longValue(); + // 判断是秒级还是毫秒级时间戳 + if (timestamp < 10000000000L) { // 秒级时间戳 + return new Date(timestamp * 1000); + } else { // 毫秒级时间戳 + return new Date(timestamp); + } + } else if (timestampObj instanceof String) { + String timestampStr = timestampObj.toString(); + if (timestampStr.contains(".")) { + // 处理浮点数时间戳 + double timestamp = Double.parseDouble(timestampStr); + return new Date((long) (timestamp * 1000)); + } else { + long timestamp = Long.parseLong(timestampStr); + if (timestamp < 10000000000L) { + return new Date(timestamp * 1000); + } else { + return new Date(timestamp); + } + } + } + } catch (Exception e) { + log.error("时间戳解析失败: {}", timestampObj, e); + } + return null; + } + + /** + * 更新设备信息 - 使用统一设备管理 + */ + @Override + @Transactional + public void updateDeviceInfo(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) { + log.warn("接收到的设备信息数据为空"); + return; + } + + String originalDeviceId = iotMsgNotifyData.getHeader().getDeviceId(); + if (StrUtil.isBlank(originalDeviceId)) { + log.warn("设备ID为空,跳过处理"); + return; + } + + try { + // 使用统一设备管理服务 + Device device = deviceManagerService.getOrCreateDevice(originalDeviceId, (newDevice) -> { + // 新设备初始化逻辑 + newDevice.setProductId(iotMsgNotifyData.getHeader().getProductId()); + if (iotMsgNotifyData.getBody() != null) { + newDevice.setStatus(iotMsgNotifyData.getBody().getStatus()); + } + }); + + // 更新设备信息 + device.setUpdateTime(new Date()); + device.setUpdateBy("系统自动更新"); + processDeviceData(device, iotMsgNotifyData); + + deviceMapper.updateById(device); + log.info("设备信息更新成功: {}", device.getDeviceId()); + + } catch (Exception e) { + log.error("更新设备信息时发生异常", e); + throw new BaseException("设备信息更新失败: " + e.getMessage()); + } + } + + /** + * 处理设备数据 + */ + private void processDeviceData(Device device, IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + Map properties = service.getProperties(); + if (properties == null) continue; + + try { + String serviceId = service.getServiceId(); + switch (serviceId) { + case "StatusChange": + processStatusChange(device, properties, iotMsgNotifyData); + break; + case "robot_info": + processRobotInfo(device, properties); + break; + case "License": + processLicenseInfo(device, properties); + break; + default: + log.debug("未知服务类型: {}", serviceId); + } + } catch (Exception e) { + log.error("处理服务数据异常,服务ID: {}", service.getServiceId(), e); + } + } + } + + /** + * 处理机器人信息 + */ + private void processRobotInfo(Device device, Map properties) { + if (properties.containsKey("serial_number")) { + device.setSerialNumber(properties.get("serial_number").toString()); + } + + if (properties.containsKey("device_location")) { + try { + Object locationObj = properties.get("device_location"); + if (locationObj instanceof Map) { + @SuppressWarnings("unchecked") + Map locationMap = (Map) locationObj; + if (!"Failed to retrieve location".equals(locationMap.get("error"))) { + device.setLongitude(locationMap.get("longitude") != null ? + locationMap.get("longitude").toString() : null); + device.setLatitude(locationMap.get("latitude") != null ? + locationMap.get("latitude").toString() : null); + } + } + } catch (Exception e) { + log.warn("处理位置信息失败", e); + } + } + } + + /** + * 处理许可证信息 + */ + private void processLicenseInfo(Device device, Map properties) { + if (properties.containsKey("is_activated")) { + boolean isActivated = Boolean.parseBoolean(properties.get("is_activated").toString()); + device.setLicenseStatus(isActivated ? "ACTIVE" : "INACTIVE"); + } + + if (properties.containsKey("activation_code")) { + device.setActivationCode(properties.get("activation_code").toString()); + } + + if (properties.containsKey("expiration_date")) { + Date expireDate = parseMillisecondTimestamp(properties.get("expiration_date")); + device.setLicenseExpireTime(expireDate); + } + + if (properties.containsKey("activated_at")) { + Date activatedAt = parseMillisecondTimestamp(properties.get("activated_at")); + if (device.getActivationTime() == null) { + device.setActivationTime(activatedAt); + } + } + } +} diff --git a/storm-device/src/main/java/com/storm/device/service/impl/DeviceShadowServiceImpl.java b/storm-device/src/main/java/com/storm/device/service/impl/DeviceShadowServiceImpl.java new file mode 100644 index 0000000..59374fc --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/impl/DeviceShadowServiceImpl.java @@ -0,0 +1,411 @@ +package com.storm.device.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.storm.common.core.utils.DateUtils; +import com.storm.device.domain.po.Device; +import com.storm.device.domain.po.DeviceRuntimeStats; +import com.storm.device.mapper.DeviceMapper; +import com.storm.device.mapper.DeviceRuntimeStatsMapper; +import com.storm.device.mapper.DeviceStatusLogMapper; +import com.storm.device.service.IDeviceShadowService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import java.util.*; + +@Slf4j +@Service +public class DeviceShadowServiceImpl implements IDeviceShadowService { + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper; + + @Autowired + private DeviceStatusLogMapper deviceStatusLogMapper; + + @Autowired + private RedisTemplate redisTemplate; + + @Override + public Map getDeviceShadowData(String deviceId) { + Map result = new HashMap<>(); + try { + // 获取设备基本信息 + Device device = deviceMapper.selectOne( + new LambdaQueryWrapper().eq(Device::getDeviceId, deviceId)); + if (device != null) { + result.put("deviceInfo", device); + } + + // 获取运行统计 + result.put("runtimeStats", getRuntimeStats(deviceId)); + + // 获取按摩统计 + result.put("massageStats", getMassageStats(deviceId, "all")); + + // 获取实时状态 + result.put("realtimeStatus", getRealtimeStatus(deviceId)); + + // 获取状态历史 + result.put("statusHistory", getStatusHistory(deviceId, 20)); + + // 获取按摩历史 + result.put("massageHistory", getMassageHistory(deviceId, 20)); + + // 获取健康状态 + result.put("healthStatus", getHealthStatus(deviceId)); + + } catch (Exception e) { + log.error("获取设备影子数据失败: {}", deviceId, e); + throw new RuntimeException("获取设备影子数据失败", e); + } + return result; + } + + @Override + public Map getRuntimeStats(String deviceId) { + Map stats = new HashMap<>(); + + try { + // 今日统计 + Date today = DateUtils.getTodayStart(); + DeviceRuntimeStats todayStats = deviceRuntimeStatsMapper.selectByDeviceAndDate(deviceId, today); + + // 本周统计 + Date weekStart = DateUtils.getWeekStart(); + Long weekDuration = deviceRuntimeStatsMapper.selectTotalDurationByPeriod(deviceId, weekStart, new Date()); + + // 本月统计 + Date monthStart = DateUtils.getMonthStart(); + Long monthDuration = deviceRuntimeStatsMapper.selectTotalDurationByPeriod(deviceId, monthStart, new Date()); + + // 总统计 + Long totalDuration = deviceRuntimeStatsMapper.selectTotalDurationByPeriod(deviceId, null, null); + + stats.put("todayDuration", todayStats != null ? todayStats.getDailyDuration() : 0); + stats.put("weekDuration", weekDuration != null ? weekDuration : 0); + stats.put("monthDuration", monthDuration != null ? monthDuration : 0); + stats.put("totalDuration", totalDuration != null ? totalDuration : 0); + + // 格式化显示 + stats.put("todayDurationFormatted", formatDuration(stats.get("todayDuration"))); + stats.put("weekDurationFormatted", formatDuration(stats.get("weekDuration"))); + stats.put("monthDurationFormatted", formatDuration(stats.get("monthDuration"))); + stats.put("totalDurationFormatted", formatDuration(stats.get("totalDuration"))); + + // 上线次数统计 + Integer todayOnlineCount = deviceRuntimeStatsMapper.selectOnlineCountByPeriod(deviceId, today, new Date()); + Integer weekOnlineCount = deviceRuntimeStatsMapper.selectOnlineCountByPeriod(deviceId, weekStart, new Date()); + Integer monthOnlineCount = deviceRuntimeStatsMapper.selectOnlineCountByPeriod(deviceId, monthStart, new Date()); + + stats.put("todayOnlineCount", todayOnlineCount != null ? todayOnlineCount : 0); + stats.put("weekOnlineCount", weekOnlineCount != null ? weekOnlineCount : 0); + stats.put("monthOnlineCount", monthOnlineCount != null ? monthOnlineCount : 0); + + } catch (Exception e) { + log.error("获取设备运行统计失败: {}", deviceId, e); + // 设置默认值 + setDefaultStats(stats); + } + + return stats; + } + + @Override + public Map getMassageStats(String deviceId, String timeRange) { + Map stats = new HashMap<>(); + + try { + Date startDate = null; + Date endDate = new Date(); + + switch (timeRange) { + case "day": + startDate = DateUtils.getTodayStart(); + break; + case "week": + startDate = DateUtils.getWeekStart(); + break; + case "month": + startDate = DateUtils.getMonthStart(); + break; + case "all": + startDate = null; + break; + default: + startDate = DateUtils.getTodayStart(); + } + + // 查询按摩任务统计 + List> massageStats = deviceStatusLogMapper.selectMassageStatsByDeviceAndPeriod( + deviceId, startDate, endDate); + + if (!massageStats.isEmpty()) { + Map stat = massageStats.get(0); + stats.put("massageCount", stat.get("massageCount")); + stats.put("totalMassageDuration", stat.get("totalDuration")); + stats.put("avgMassageDuration", stat.get("avgDuration")); + + // 格式化显示 + stats.put("totalMassageDurationFormatted", formatDuration(stat.get("totalDuration"))); + stats.put("avgMassageDurationFormatted", formatDuration(stat.get("avgDuration"))); + } else { + setDefaultMassageStats(stats); + } + + // 按头类型统计 + List> headTypeStats = deviceStatusLogMapper.selectHeadTypeStatsByDevice( + deviceId, startDate, endDate); + stats.put("headTypeStats", headTypeStats); + + // 按部位统计 + List> bodyPartStats = deviceStatusLogMapper.selectBodyPartStatsByDevice( + deviceId, startDate, endDate); + stats.put("bodyPartStats", bodyPartStats); + + // 最近7天按摩趋势 + List> trendStats = deviceStatusLogMapper.selectMassageTrend(deviceId, 7); + stats.put("trendStats", trendStats); + + } catch (Exception e) { + log.error("获取设备按摩统计失败: {}", deviceId, e); + setDefaultMassageStats(stats); + } + + return stats; + } + + @Override + public Map getRealtimeStatus(String deviceId) { + Map status = new HashMap<>(); + + try { + String statusKey = "device:status:" + deviceId; + String lastOnlineKey = "device:last_online:" + deviceId; + + String currentStatus = (String) redisTemplate.opsForValue().get(statusKey); + String lastOnlineTimestamp = (String) redisTemplate.opsForValue().get(lastOnlineKey); + + // 查询设备信息 + Device device = deviceMapper.selectOne( + new LambdaQueryWrapper().eq(Device::getDeviceId, deviceId)); + + if (device != null) { + status.put("deviceInfo", device); + status.put("softwareVersion", device.getSoftwareVersion()); + status.put("vtxdbVersion", device.getVtxdbVersion()); + status.put("licenseStatus", device.getLicenseStatus()); + status.put("lastMassageTime", device.getLastMassageTime()); + } + + status.put("status", currentStatus != null ? currentStatus : "UNKNOWN"); + status.put("lastOnlineTime", lastOnlineTimestamp != null ? + new Date(Long.parseLong(lastOnlineTimestamp)) : null); + status.put("updateTime", new Date()); + + // 计算离线时长(如果设备离线) + if ("OFFLINE".equals(currentStatus) && lastOnlineTimestamp != null) { + long offlineDuration = System.currentTimeMillis() - Long.parseLong(lastOnlineTimestamp); + status.put("offlineDuration", offlineDuration); + status.put("offlineDurationFormatted", formatDuration(offlineDuration)); + } + + } catch (Exception e) { + log.error("获取设备实时状态失败: {}", deviceId, e); + status.put("status", "ERROR"); + status.put("error", "获取状态失败"); + } + + return status; + } + + @Override + public List> getStatusHistory(String deviceId, int limit) { + try { + return deviceStatusLogMapper.selectStatusHistory(deviceId, limit); + } catch (Exception e) { + log.error("获取设备状态历史失败: {}", deviceId, e); + return Collections.emptyList(); + } + } + + @Override + public List> getMassageHistory(String deviceId, int limit) { + try { + return deviceStatusLogMapper.selectMassageHistory(deviceId, limit); + } catch (Exception e) { + log.error("获取设备按摩历史失败: {}", deviceId, e); + return Collections.emptyList(); + } + } + + @Override + public Map getHealthStatus(String deviceId) { + Map healthStatus = new HashMap<>(); + + try { + // 获取设备信息 + Device device = deviceMapper.selectOne( + new LambdaQueryWrapper().eq(Device::getDeviceId, deviceId)); + + if (device == null) { + healthStatus.put("status", "NOT_FOUND"); + healthStatus.put("message", "设备不存在"); + return healthStatus; + } + + // 检查在线状态 + String statusKey = "device:status:" + deviceId; + String currentStatus = (String) redisTemplate.opsForValue().get(statusKey); + + healthStatus.put("deviceStatus", currentStatus != null ? currentStatus : "UNKNOWN"); + healthStatus.put("licenseStatus", device.getLicenseStatus()); + healthStatus.put("softwareVersion", device.getSoftwareVersion()); + healthStatus.put("lastOnlineTime", device.getLastOnlineTime()); + healthStatus.put("lastMassageTime", device.getLastMassageTime()); + + // 计算健康分数(示例逻辑) + int healthScore = calculateHealthScore(device, currentStatus); + healthStatus.put("healthScore", healthScore); + + // 健康等级 + String healthLevel = getHealthLevel(healthScore); + healthStatus.put("healthLevel", healthLevel); + + // 建议措施 + List suggestions = getHealthSuggestions(device, currentStatus, healthScore); + healthStatus.put("suggestions", suggestions); + + } catch (Exception e) { + log.error("获取设备健康状态失败: {}", deviceId, e); + healthStatus.put("status", "ERROR"); + healthStatus.put("message", "获取健康状态失败"); + } + + return healthStatus; + } + + // 私有辅助方法 + private String formatDuration(Object durationObj) { + if (durationObj == null) return "0秒"; + + long milliseconds = 0; + if (durationObj instanceof Number) { + milliseconds = ((Number) durationObj).longValue(); + } else if (durationObj instanceof String) { + try { + milliseconds = Long.parseLong(durationObj.toString()); + } catch (NumberFormatException e) { + return "0秒"; + } + } + + return formatMilliseconds(milliseconds); + } + + private String formatMilliseconds(long milliseconds) { + if (milliseconds == 0) return "0秒"; + + long seconds = milliseconds / 1000; + long hours = seconds / 3600; + long minutes = (seconds % 3600) / 60; + long secs = seconds % 60; + + if (hours > 0) { + return String.format("%d小时%d分%d秒", hours, minutes, secs); + } else if (minutes > 0) { + return String.format("%d分%d秒", minutes, secs); + } else { + return String.format("%d秒", secs); + } + } + + private void setDefaultStats(Map stats) { + stats.put("todayDuration", 0); + stats.put("weekDuration", 0); + stats.put("monthDuration", 0); + stats.put("totalDuration", 0); + stats.put("todayDurationFormatted", "0秒"); + stats.put("weekDurationFormatted", "0秒"); + stats.put("monthDurationFormatted", "0秒"); + stats.put("totalDurationFormatted", "0秒"); + stats.put("todayOnlineCount", 0); + stats.put("weekOnlineCount", 0); + stats.put("monthOnlineCount", 0); + } + + private void setDefaultMassageStats(Map stats) { + stats.put("massageCount", 0); + stats.put("totalMassageDuration", 0); + stats.put("avgMassageDuration", 0); + stats.put("totalMassageDurationFormatted", "0秒"); + stats.put("avgMassageDurationFormatted", "0秒"); + stats.put("headTypeStats", Collections.emptyList()); + stats.put("bodyPartStats", Collections.emptyList()); + stats.put("trendStats", Collections.emptyList()); + } + + private int calculateHealthScore(Device device, String currentStatus) { + int score = 100; + + // 在线状态扣分 + if (!"ONLINE".equals(currentStatus)) { + score -= 30; + } + + // 许可证状态扣分 + if (!"ACTIVE".equals(device.getLicenseStatus())) { + score -= 20; + } + + // 长时间未按摩扣分 + if (device.getLastMassageTime() != null) { + long daysSinceLastMassage = (System.currentTimeMillis() - device.getLastMassageTime().getTime()) + / (1000 * 60 * 60 * 24); + if (daysSinceLastMassage > 7) { + score -= 10; + } + } + + return Math.max(score, 0); + } + + private String getHealthLevel(int healthScore) { + if (healthScore >= 90) return "优秀"; + if (healthScore >= 70) return "良好"; + if (healthScore >= 50) return "一般"; + return "需要关注"; + } + + private List getHealthSuggestions(Device device, String currentStatus, int healthScore) { + List suggestions = new ArrayList<>(); + + if (!"ONLINE".equals(currentStatus)) { + suggestions.add("设备当前离线,请检查网络连接和设备状态"); + } + + if (!"ACTIVE".equals(device.getLicenseStatus())) { + suggestions.add("设备许可证未激活或已过期,请及时处理"); + } + + if (device.getLastMassageTime() == null) { + suggestions.add("设备尚未开始使用,请引导用户使用按摩功能"); + } else { + long daysSinceLastMassage = (System.currentTimeMillis() - device.getLastMassageTime().getTime()) + / (1000 * 60 * 60 * 24); + if (daysSinceLastMassage > 7) { + suggestions.add("设备已" + daysSinceLastMassage + "天未使用,建议进行设备检查"); + } + } + + if (healthScore < 70) { + suggestions.add("设备健康度较低,建议进行全面检查和维护"); + } + + return suggestions; + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/service/impl/DeviceStatusLogServiceImpl.java b/storm-device/src/main/java/com/storm/device/service/impl/DeviceStatusLogServiceImpl.java new file mode 100644 index 0000000..53be4d8 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/impl/DeviceStatusLogServiceImpl.java @@ -0,0 +1,658 @@ +package com.storm.device.service.impl; + +import java.util.*; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.storm.device.domain.po.Device; +import com.storm.device.domain.po.DeviceHeadUsage; +import com.storm.device.domain.po.DeviceStatusLog; +import com.storm.device.domain.po.MassageTask; +import com.storm.device.manager.DeviceManagerService; +import com.storm.device.mapper.DeviceStatusLogMapper; +import com.storm.device.service.IDeviceStatusLogService; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +/** + * 设备状态日志Service业务层处理 + * + * @author storm + * @date 2025-08-28 + */ +@Slf4j +@Service +public class DeviceStatusLogServiceImpl extends ServiceImpl implements IDeviceStatusLogService { + @Autowired + private DeviceStatusLogMapper deviceStatusLogMapper; + @Autowired + private DeviceManagerService deviceManagerService; + + @Override + public List selectDeviceStatusLogList(DeviceStatusLog deviceStatusLog) { + return deviceStatusLogMapper.selectList(Wrappers.lambdaQuery() + .eq(deviceStatusLog.getDeviceId() != null, DeviceStatusLog::getDeviceId, deviceStatusLog.getDeviceId()) + .eq(deviceStatusLog.getStatus() != null, DeviceStatusLog::getStatus, deviceStatusLog.getStatus()) + .eq(deviceStatusLog.getEventTime() != null, DeviceStatusLog::getEventTime, deviceStatusLog.getEventTime()) + .eq(deviceStatusLog.getLastOnlineTime() != null, DeviceStatusLog::getLastOnlineTime, deviceStatusLog.getLastOnlineTime()) + .eq(deviceStatusLog.getDuration() != null, DeviceStatusLog::getDuration, deviceStatusLog.getDuration())); + } + + /** + * 更新设备状态日志 - 使用统一设备管理 + */ + @Override + public void updateDeviceStatusLog(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) { + return; + } + + String originalDeviceId = iotMsgNotifyData.getHeader().getDeviceId(); + if (StrUtil.isBlank(originalDeviceId)) { + log.warn("设备ID为空,跳过状态日志处理"); + return; + } + + try { + // 确保设备存在 + Device device = deviceManagerService.getOrCreateDevice(originalDeviceId, null); + String normalizedDeviceId = device.getDeviceId(); + + // 检查是否已存在相同记录 + if (isDuplicateStatusLog(normalizedDeviceId, iotMsgNotifyData)) { + log.debug("重复的状态日志,跳过处理: {}", normalizedDeviceId); + return; + } + + DeviceStatusLog statusLog = createStatusLog(normalizedDeviceId, iotMsgNotifyData); + boolean saveResult = this.save(statusLog); + + if (saveResult) { + log.info("设备状态日志保存成功,设备: {},状态: {}", normalizedDeviceId, statusLog.getStatus()); + } else { + log.error("设备状态日志保存失败,设备: {}", normalizedDeviceId); + } + + } catch (Exception e) { + log.error("处理设备状态日志时发生异常", e); + } + } + + /** + * 检查是否重复状态日志 + */ + private boolean isDuplicateStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null) { + return false; + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(DeviceStatusLog::getDeviceId, deviceId); + + if (iotMsgNotifyData.getBody().getStatusUpdateTime() != null) { + queryWrapper.eq(DeviceStatusLog::getEventTime, iotMsgNotifyData.getBody().getStatusUpdateTime()); + } + + if (StrUtil.isNotBlank(iotMsgNotifyData.getBody().getStatus())) { + queryWrapper.eq(DeviceStatusLog::getStatus, iotMsgNotifyData.getBody().getStatus()); + } + + return this.count(queryWrapper) > 0; + } + /** + * 创建状态日志记录 + */ + private DeviceStatusLog createStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + DeviceStatusLog statusLog = new DeviceStatusLog(); + statusLog.setDeviceId(deviceId); + statusLog.setCreateTime(new Date()); + statusLog.setEventType("STATUS_CHANGE"); + + if (iotMsgNotifyData.getBody() != null) { + // 处理状态信息 + if (StrUtil.isNotBlank(iotMsgNotifyData.getBody().getStatus())) { + String status = iotMsgNotifyData.getBody().getStatus(); + statusLog.setStatus("ONLINE".equalsIgnoreCase(status) ? "ONLINE" : "OFFLINE"); + } + + // 处理事件时间 + if (iotMsgNotifyData.getBody().getStatusUpdateTime() != null) { + try { + Date statusUpdateTime = iotMsgNotifyData.getBody().getStatusUpdateTime(); + statusLog.setEventTime(statusUpdateTime); + } catch (Exception e) { + log.warn("状态更新时间解析失败,使用当前时间: {}", iotMsgNotifyData.getBody().getStatusUpdateTime()); + statusLog.setEventTime(new Date()); + } + } else { + statusLog.setEventTime(new Date()); + } + + // 处理最后在线时间 + if (iotMsgNotifyData.getBody().getLastOnlineTime() != null) { + try { + Date lastOnlineTime = iotMsgNotifyData.getBody().getLastOnlineTime(); + statusLog.setLastOnlineTime(lastOnlineTime); + } catch (Exception e) { + log.warn("最后在线时间解析失败: {}", iotMsgNotifyData.getBody().getLastOnlineTime()); + } + } + + // 处理服务数据中的额外属性 + if (iotMsgNotifyData.getBody().getServices() != null) { + processServiceProperties(statusLog, iotMsgNotifyData.getBody().getServices()); + } + } + + return statusLog; + } + + /** + * 处理服务属性 + */ + private void processServiceProperties(DeviceStatusLog statusLog, List services) { + for (IotMsgNotifyDataPro.IotMsgService service : services) { + if (service.getProperties() == null) continue; + + Map properties = service.getProperties(); + + // 处理按摩相关属性 + if (properties.containsKey("head_type")) { + statusLog.setHeadType(properties.get("head_type").toString()); + } + if (properties.containsKey("body_part")) { + statusLog.setBodyPart(properties.get("body_part").toString()); + } + if (properties.containsKey("massage_plan")) { + statusLog.setMassagePlan(properties.get("massage_plan").toString()); + } + + // 处理按摩时间 + if (properties.containsKey("massage_start_time")) { + Long startTime = parseTimestampToMillis(properties.get("massage_start_time")); + statusLog.setMassageStartTime(startTime); + } + if (properties.containsKey("massage_end_time")) { + Long endTime = parseTimestampToMillis(properties.get("massage_end_time")); + statusLog.setMassageEndTime(endTime); + + // 计算持续时间 + if (statusLog.getMassageStartTime() != null && endTime != null) { + statusLog.setMassageDuration(endTime - statusLog.getMassageStartTime()); + } + } + + // 如果是按摩任务,设置事件类型 + if (properties.containsKey("massage_start_time") || + properties.containsKey("head_type") || + properties.containsKey("body_part")) { + statusLog.setEventType("MASSAGE_TASK"); + } + } + } + + /** + * 解析时间戳为毫秒 + */ + private Long parseTimestampToMillis(Object timestampObj) { + if (timestampObj == null) return null; + + try { + if (timestampObj instanceof Number) { + long timestamp = ((Number) timestampObj).longValue(); + if (timestamp < 10000000000L) { // 秒级时间戳 + return timestamp * 1000; + } else { // 毫秒级时间戳 + return timestamp; + } + } else if (timestampObj instanceof String) { + String timestampStr = timestampObj.toString(); + if (timestampStr.contains(".")) { + // 处理浮点数时间戳 + double timestamp = Double.parseDouble(timestampStr); + return (long) (timestamp * 1000); + } else { + long timestamp = Long.parseLong(timestampStr); + if (timestamp < 10000000000L) { + return timestamp * 1000; + } else { + return timestamp; + } + } + } + } catch (Exception e) { + log.error("时间戳解析失败: {}", timestampObj, e); + } + return null; + } + + /** + * 安全插入状态日志 - 多层保护机制 + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public boolean safeInsertStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + for (int attempt = 0; attempt < 3; attempt++) { + try { + return doInsertStatusLog(deviceId, iotMsgNotifyData, attempt); + } catch (DuplicateKeyException e) { + log.debug("状态日志重复键异常,尝试次数: {},设备: {}", attempt + 1, deviceId); + if (attempt == 2) { + log.warn("状态日志插入最终失败(重复键),设备: {}", deviceId); + return false; // 重复数据可以安全忽略 + } + // 短暂等待后重试 + try { + Thread.sleep(50 * (attempt + 1)); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return false; + } + } catch (Exception e) { + log.error("状态日志插入异常,尝试次数: {},设备: {}", attempt + 1, deviceId, e); + if (attempt == 2) { + return false; + } + } + } + return false; + } + + private boolean doInsertStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData, int attempt) { + // 先检查是否已存在相同记录 + if (attempt == 0 && isStatusLogExists(deviceId, iotMsgNotifyData)) { + log.debug("状态日志已存在,跳过插入: {}", deviceId); + return true; // 已存在也算成功 + } + + DeviceStatusLog statusLog = createStatusLogFromData(deviceId, iotMsgNotifyData); + if (statusLog == null) { + log.warn("创建状态日志对象失败: {}", deviceId); + return false; + } + + // 根据尝试次数选择不同的插入策略 + switch (attempt) { + case 0: + // 方法1: 使用 INSERT IGNORE(最安全) + return insertWithIgnore(statusLog); + case 1: + // 方法2: 使用 INSERT ... ON DUPLICATE KEY UPDATE + return insertOrUpdate(statusLog); + case 2: + // 方法3: 先删除后插入(最终保障) + return insertWithDeleteFirst(statusLog); + default: + return false; + } + } + + /** + * 检查状态日志是否已存在 + */ + private boolean isStatusLogExists(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + if (iotMsgNotifyData.getBody() == null) { + return false; + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(DeviceStatusLog::getDeviceId, deviceId); + + // 使用事件时间查询 + if (iotMsgNotifyData.getBody().getStatusUpdateTime() != null) { + queryWrapper.eq(DeviceStatusLog::getEventTime, iotMsgNotifyData.getBody().getStatusUpdateTime()); + } + + // 使用状态查询 + if (iotMsgNotifyData.getBody().getStatus() != null) { + String status = iotMsgNotifyData.getBody().getStatus(); + queryWrapper.eq(DeviceStatusLog::getStatus, "ONLINE".equalsIgnoreCase(status) ? "ONLINE" : "OFFLINE"); + } + + return deviceStatusLogMapper.selectCount(queryWrapper) > 0; + } catch (Exception e) { + log.warn("检查状态日志是否存在失败: {}", deviceId, e); + return false; + } + } + + /** + * 从消息数据创建状态日志对象 + */ + private DeviceStatusLog createStatusLogFromData(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + DeviceStatusLog statusLog = new DeviceStatusLog(); + statusLog.setDeviceId(deviceId); + statusLog.setCreateTime(new Date()); + statusLog.setEventType("STATUS_CHANGE"); + + if (iotMsgNotifyData.getBody() == null) { + return statusLog; + } + + // 设置状态 + if (iotMsgNotifyData.getBody().getStatus() != null) { + String status = iotMsgNotifyData.getBody().getStatus(); + statusLog.setStatus("ONLINE".equalsIgnoreCase(status) ? "ONLINE" : "OFFLINE"); + } + + // 设置事件时间 + if (iotMsgNotifyData.getBody().getStatusUpdateTime() != null) { + statusLog.setEventTime(iotMsgNotifyData.getBody().getStatusUpdateTime()); + } else { + statusLog.setEventTime(new Date()); + } + + // 设置最后在线时间 + if (iotMsgNotifyData.getBody().getLastOnlineTime() != null) { + statusLog.setLastOnlineTime(iotMsgNotifyData.getBody().getLastOnlineTime()); + } + // 设置其他字段 + statusLog.setCreateBy("系统自动创建"); + List services = iotMsgNotifyData.getBody().getServices(); + if(services!=null&&services.size()>0){ + for (IotMsgNotifyDataPro.IotMsgService service : services) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + Map props = service.getProperties(); + Long startTime = safeParseTimestamp(props.get("massage_start_time")); + Long endTime = safeParseTimestamp(props.get("massage_end_time")); + statusLog.setMassageStartTime(startTime); + statusLog.setMassageEndTime(endTime); + statusLog.setHeadType(safeGetString(props, "head_type")); + statusLog.setBodyPart(safeGetString(props, "body_part")); + if (startTime != null && endTime != null && startTime < endTime && startTime > 0) { + long duration = (endTime - startTime); + statusLog.setDuration((int)duration); + statusLog.setMassageDuration(duration); + } + } + } + } + // 处理服务数据 + processServiceData(statusLog, iotMsgNotifyData); + + return statusLog; + } + + private String safeGetString(Map props, String key) { + if (props == null || !props.containsKey(key)) return null; + Object value = props.get(key); + return value != null ? value.toString() : null; + } + + /** + * 处理服务数据 + */ + private void processServiceData(DeviceStatusLog statusLog, IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData.getBody() == null || iotMsgNotifyData.getBody().getServices() == null) { + return; + } + + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId()) && service.getProperties() != null) { + // 处理按摩相关数据 + processMassageData(statusLog, service.getProperties()); + break; + } + } + } + + /** + * 处理按摩数据 - 修复版本 + */ + private void processMassageData(DeviceStatusLog statusLog, java.util.Map properties) { + if (properties.containsKey("head_type") && properties.get("head_type") != null) { + statusLog.setHeadType(properties.get("head_type").toString()); + } + if (properties.containsKey("body_part") && properties.get("body_part") != null) { + statusLog.setBodyPart(properties.get("body_part").toString()); + } + if (properties.containsKey("massage_plan") && properties.get("massage_plan") != null) { + statusLog.setMassagePlan(properties.get("massage_plan").toString()); + } + + // 处理按摩时间 - 使用安全解析 + Long startTime = safeParseTimestamp(properties.get("massage_start_time")); + Long endTime = safeParseTimestamp(properties.get("massage_end_time")); + + if (startTime != null) { + statusLog.setMassageStartTime(startTime); + } + if (endTime != null) { + statusLog.setMassageEndTime(endTime); + } + + // 安全计算持续时间 + Long duration = safeCalculateDuration(startTime, endTime); + if (duration != null) { + statusLog.setMassageDuration(duration); + statusLog.setDuration(duration.intValue()); + + // 如果是按摩任务,更改事件类型 + statusLog.setEventType("MASSAGE_TASK"); + log.debug("计算按摩持续时间: {} 秒", duration); + } else { + // 设置默认值 + statusLog.setMassageDuration(0L); + statusLog.setDuration(0); + log.warn("无法计算有效的按摩持续时间,startTime: {}, endTime: {}", startTime, endTime); + } + } + + private boolean insertWithIgnore(DeviceStatusLog statusLog) { + try { + int result = deviceStatusLogMapper.insertOrIgnore(statusLog); + if (result > 0) { + log.debug("INSERT IGNORE 成功: {}", statusLog.getDeviceId()); + return true; + } else { + log.debug("INSERT IGNORE 忽略重复记录: {}", statusLog.getDeviceId()); + return true; // 忽略重复也算成功 + } + } catch (Exception e) { + log.warn("INSERT IGNORE 失败: {}", statusLog.getDeviceId(), e); + throw e; + } + } + + private boolean insertOrUpdate(DeviceStatusLog statusLog) { + try { + int result = deviceStatusLogMapper.insertOrUpdate(statusLog); + log.debug("INSERT OR UPDATE 结果: {} -> {}", statusLog.getDeviceId(), result); + return result > 0; + } catch (Exception e) { + log.warn("INSERT OR UPDATE 失败: {}", statusLog.getDeviceId(), e); + throw e; + } + } + + private boolean insertWithDeleteFirst(DeviceStatusLog statusLog) { + try { + // 先删除可能存在的重复记录 + LambdaQueryWrapper deleteWrapper = new LambdaQueryWrapper<>(); + deleteWrapper.eq(DeviceStatusLog::getDeviceId, statusLog.getDeviceId()) + .eq(DeviceStatusLog::getEventTime, statusLog.getEventTime()) + .eq(DeviceStatusLog::getStatus, statusLog.getStatus()); + + deviceStatusLogMapper.delete(deleteWrapper); + + // 然后插入新记录 + int result = deviceStatusLogMapper.insert(statusLog); + return result > 0; + } catch (Exception e) { + log.error("先删后插方案失败: {}", statusLog.getDeviceId(), e); + return false; + } + } + + private Long parseTimestamp(Object timestampObj) { + if (timestampObj == null) return null; + try { + if (timestampObj instanceof Number) { + long timestamp = ((Number) timestampObj).longValue(); + // 判断是秒级还是毫秒级 + if (timestamp < 10000000000L) { // 秒级 + return timestamp * 1000; + } else { // 毫秒级 + return timestamp; + } + } else if (timestampObj instanceof String) { + return Long.parseLong(timestampObj.toString()); + } + } catch (Exception e) { + log.warn("时间戳解析失败: {}", timestampObj); + } + return null; + } + + /** + * 安全解析时间戳 - 修复版本 + */ + private Long safeParseTimestamp(Object timestampObj) { + if (timestampObj == null) return null; + + try { + long timestamp; + if (timestampObj instanceof Number) { + timestamp = ((Number) timestampObj).longValue(); + } else if (timestampObj instanceof String) { + String timestampStr = timestampObj.toString(); + // 处理科学计数法 + if (timestampStr.contains("E") || timestampStr.contains("e")) { + double doubleValue = Double.parseDouble(timestampStr); + timestamp = (long) doubleValue; + } else { + timestamp = Long.parseLong(timestampStr); + } + } else { + return null; + } + + // 验证时间戳的有效性 + if (!isValidTimestamp(timestamp)) { + log.warn("无效的时间戳: {}", timestamp); + return null; + } + + // 判断是秒级还是毫秒级 + if (timestamp < 10000000000L) { // 秒级时间戳(小于 2286-11-21) + return timestamp * 1000; + } else { // 毫秒级时间戳 + return timestamp; + } + } catch (Exception e) { + log.warn("时间戳解析失败: {}", timestampObj, e); + return null; + } + } + + /** + * 安全计算按摩持续时间 + */ + private Long safeCalculateDuration(Long startTime, Long endTime) { + if (startTime == null || endTime == null) { + return null; + } + + // 验证时间戳有效性 + if (!isValidTimestamp(startTime) || !isValidTimestamp(endTime)) { + log.warn("无效的时间戳,startTime: {}, endTime: {}", startTime, endTime); + return null; + } + + // 确保 startTime 和 endTime 都是毫秒级 + if (startTime < 10000000000L) { + startTime = startTime * 1000; + } + if (endTime < 10000000000L) { + endTime = endTime * 1000; + } + + // 检查时间顺序 + if (startTime >= endTime) { + log.warn("开始时间大于等于结束时间,startTime: {}, endTime: {}", startTime, endTime); + return null; + } + + // 检查持续时间是否合理(最长24小时) + long durationMs = endTime - startTime; + long maxValidDuration = 24 * 60 * 60 * 1000L; // 24小时 + + if (durationMs > maxValidDuration) { + log.warn("持续时间过长: {} 毫秒,超过最大限制 {}", durationMs, maxValidDuration); + return null; + } + + return durationMs / 1000; // 返回秒数 + } + + /** + * 验证时间戳是否有效 + */ + private boolean isValidTimestamp(long timestamp) { + // 检查是否为0或负数 + if (timestamp <= 0) { + return false; + } + + // 检查是否在合理范围内(1970年 - 2100年) + long minValidTimestamp = 0L; // 1970-01-01 + long maxValidTimestamp = 4102444800000L; // 2100-01-01 的毫秒时间戳 + + // 如果是秒级时间戳,转换为毫秒后检查 + if (timestamp < 10000000000L) { + timestamp = timestamp * 1000; + } + + return timestamp >= minValidTimestamp && timestamp <= maxValidTimestamp; + } + + // 在 DeviceStatusLogService 中添加更精确的检查方法 + /** + * 精确检查状态日志是否重复 + */ + private boolean isExactDuplicate(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) { + try { + if (iotMsgNotifyData.getBody() == null) { + return false; + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(DeviceStatusLog::getDeviceId, deviceId); + + // 精确匹配事件时间(精确到秒) + if (iotMsgNotifyData.getBody().getStatusUpdateTime() != null) { + // 将时间精确到秒级进行比较 + Date eventTime = iotMsgNotifyData.getBody().getStatusUpdateTime(); + long eventTimeSeconds = eventTime.getTime() / 1000; + + queryWrapper.apply("UNIX_TIMESTAMP(event_time) = {0}", eventTimeSeconds); + } + + // 精确匹配状态 + if (iotMsgNotifyData.getBody().getStatus() != null) { + String status = iotMsgNotifyData.getBody().getStatus(); + queryWrapper.eq(DeviceStatusLog::getStatus, "ONLINE".equalsIgnoreCase(status) ? "ONLINE" : "OFFLINE"); + } + + int count = (deviceStatusLogMapper.selectCount(queryWrapper)).intValue(); + boolean isDuplicate = count > 0; + + if (isDuplicate) { + log.debug("检测到精确重复的状态日志: {}", deviceId); + } + + return isDuplicate; + + } catch (Exception e) { + log.warn("精确重复检查失败: {}", deviceId, e); + return false; + } + } +} diff --git a/storm-device/src/main/java/com/storm/device/service/impl/MassageTaskServiceImpl.java b/storm-device/src/main/java/com/storm/device/service/impl/MassageTaskServiceImpl.java new file mode 100644 index 0000000..f474f7f --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/service/impl/MassageTaskServiceImpl.java @@ -0,0 +1,562 @@ +package com.storm.device.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.huaweicloud.sdk.iotda.v5.IoTDAClient; +import com.huaweicloud.sdk.iotda.v5.model.ListDevicesRequest; +import com.huaweicloud.sdk.iotda.v5.model.ListDevicesResponse; +import com.storm.common.core.utils.DateUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.device.domain.dto.MassageTaskStatDTO; +import com.storm.device.domain.po.Device; +import com.storm.device.domain.po.MassageTask; +import com.storm.device.manager.DeviceManagerService; +import com.storm.device.mapper.MassageTaskMapper; +import com.storm.device.service.IMassageTaskService; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import javax.annotation.Resource; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * 按摩任务记录Service业务层处理 + * + * @author storm + * @date 2025-08-28 + */ +@Slf4j +@Service +public class MassageTaskServiceImpl extends ServiceImpl implements IMassageTaskService { + + @Resource + private MassageTaskMapper massageTaskMapper; + + @Autowired + private DeviceManagerService deviceManagerService; + + @Resource + private IoTDAClient iotdaClient; + + /** + * 更新按摩任务 - 主要方法(使用 IotMsgNotifyDataPro) + */ + @Override + public void updateMassageTask(IotMsgNotifyDataPro iotMsgNotifyData) { + if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) { + return; + } + + String deviceId = iotMsgNotifyData.getHeader().getDeviceId(); + if (StrUtil.isBlank(deviceId)) { + log.warn("设备ID为空,跳过按摩任务处理"); + return; + } + + if (iotMsgNotifyData.getBody() != null && iotMsgNotifyData.getBody().getServices() != null) { + for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) { + if ("StatusChange".equals(service.getServiceId())) { + processMassageTask(deviceId, service); + } + } + } + } + + /** + * 获取按摩头类型统计 + * + * @param startTime + * @param endTime + * @return + */ + @Override + public List> getHeadTypeStats(String startTime, String endTime) { + log.info("查询按摩头类型统计数据,startTime:{},endTime:{}", startTime, endTime); + return massageTaskMapper.countByHeadType(startTime, endTime); + } + + /** + * 获取按摩部位统计 + * + * @param startTime + * @param endTime + * @return + */ + @Override + public List> getBodyPartStats(String startTime, String endTime) { + log.info("查询按摩部位统计数据,startTime:{},endTime:{}", startTime, endTime); + return massageTaskMapper.countByBodyPart(startTime, endTime); + } + + /** + * 热门使用时间热力图统计 + * + * @param startTime + * @param endTime + * @return + */ + @Override + public List getMassageTaskStatsByRange(String startTime, String endTime) { + log.info("查询热门使用时间热力图统计数据,startTime:{},endTime:{}", startTime, endTime); + return massageTaskMapper.getMassageTaskStatsByRange(startTime, endTime); + } + + /** + * 设备总按摩时间统计 + * + * @param startTime + * @param endTime + * @return + */ + @Override + public List> getDeviceMassageStats(String startTime, String endTime) { + log.info("查询设备总按摩时间统计数据,startTime:{},endTime:{}", startTime, endTime); + return massageTaskMapper.getDeviceMassageStats(startTime, endTime); + } + + /** + * 获取数据最小时间 + * + * @return + */ + @Override + public LocalDate getMinDate() { + String minDateStr = massageTaskMapper.getMinDate(); + + return StringUtils.hasText(minDateStr) ? LocalDate.parse(minDateStr) : null; + } + + /** + * 查询每日的按摩总时长 + * + * @param startTime + * @param endTime + * @return + */ + @Override + public List> getDailyStats(String startTime, String endTime) { + log.info("查询每日的按摩总时长统计数据,startTime:{},endTime:{}", startTime, endTime); + // 查询有数据的日期 + List> rawStats = massageTaskMapper.getDailyStats(startTime, endTime); + + // 转换为Map,方便后续查找 + Map> statsMap = new HashMap<>(); + for (Map stat : rawStats) { + statsMap.put((String) stat.get("date"), stat); + } + + // 补全日期,按天 + List allDates = DateUtils.getDateRange(startTime, endTime); + List> result = new ArrayList<>(); + + for (String date : allDates) { + Map dayStat = statsMap.getOrDefault(date, new HashMap<>()); + Map item = new HashMap<>(); + item.put("date", date); + item.put("totalDuration", dayStat.getOrDefault("totalDuration", 0)); + item.put("avgDuration", dayStat.getOrDefault("avgDuration", 0)); + result.add(item); + } + + return result; + } + + /** + * 分析按摩总时长的环比增长趋势 + * + * @param startTime + * @param endTime + * @return + */ + @Override + public AjaxResult getTrendInsight(String startTime, String endTime) { + try { + log.info("分析按摩总时长的环比增长趋势,startTime:{},endTime:{}", startTime, endTime); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + // 没传就设为 null,不默认当天,便于 mapper SQL里 判断 + LocalDate startDate = StringUtils.isNotBlank(startTime) ? LocalDate.parse(startTime, formatter) : null; + LocalDate endDate = StringUtils.isNotBlank(endTime) ? LocalDate.parse(endTime, formatter) : null; + + // 1. 获取当前时间段的最高峰日期 + String peakDate = massageTaskMapper.getPeakDate( + startDate != null ? startDate.toString() : null, + endDate != null ? endDate.toString() : null + ); + + // 2. 获取上个月同期时间段 + String lastMonthStart = startDate != null ? startDate.minusMonths(1).toString() : null; + String lastMonthEnd = endDate != null ? endDate.minusMonths(1).toString() : null; + + // 3. 查询上个月同期总时长 + Double lastMonthDuration = massageTaskMapper.getTotalDuration(lastMonthStart, lastMonthEnd); + if (lastMonthDuration == null) { + lastMonthDuration = 0.0; + } + + // 4. 当前时间段总时长 + Double currentDuration = massageTaskMapper.getTotalDuration( + startDate != null ? startDate.toString() : null, + endDate != null ? endDate.toString() : null + ); + if (currentDuration == null) { + currentDuration = 0.0; + } + // 5. 环比增长率计算 + double growthRate = 0; + if (lastMonthDuration != null && lastMonthDuration > 0) { + growthRate = ((currentDuration - lastMonthDuration) / lastMonthDuration) * 100; + } else { + growthRate = 0; // 或者返回 null 或 "N/A",根据前端展示需要 + } + + // 6. 封装返回 + Map result = new HashMap<>(); + result.put("peakDate", peakDate); + result.put("growthRate", growthRate); + + return AjaxResult.success(result); + + } catch (Exception e) { + e.printStackTrace(); + return AjaxResult.error("查询出错:" + e.getMessage()); + } + } + + @Override + public Map getStats() { + log.info("查询按摩任务统计数据"); + Map data = massageTaskMapper.getStats(); + Long totalTimes = ((Number) data.get("totalTimes")).longValue(); + Long diffDays = 0L; + if(data.get("diffDays")!=null){ + diffDays = ((Number) data.get("diffDays")).longValue(); + } + // 计算平均每天按摩次数 + Long avgTimesPerDay = Math.round(totalTimes * 1.0 / diffDays); + + data.put("avgTimesPerDay", avgTimesPerDay); + + // 计算非活跃设备数 + ListDevicesRequest request = new ListDevicesRequest(); + + ListDevicesResponse response = iotdaClient.listDevices(request); + + Long totalDev = response.getPage().getCount();//((Number) data.get("totalDev")).longValue(); + + + Long actDev = ((Number) data.get("actDev")).longValue(); + Long inactDev = totalDev - actDev; + + data.put("inactDev", inactDev); + + return data; + } + + /** + * 按摩任务的总计数据 + * + * @return + */ + @Override + public Map getTotals() { + log.info("按摩任务的总计数据"); + Map stats = new HashMap<>(); + Long massageTaskTotal = massageTaskMapper.getMassageTaskTotal(); + Long deviceOnlineTotal = massageTaskMapper.getReportEvent(); + + stats.put("massageTaskTotal", massageTaskTotal); + stats.put("reportEventTotal", deviceOnlineTotal + massageTaskTotal); + return stats; + } + + /** + * 查询按摩时间上报记录列表 + * + * @param massageTask + * @return + */ + @Override + public List selectMassageTaskList(MassageTask massageTask) { + return massageTaskMapper.selectMassageTaskList(massageTask); + } + + private void processMassageTask(String deviceId, IotMsgNotifyDataPro.IotMsgService service) { + Map properties = service.getProperties(); + if (properties == null) return; + + try { + boolean hasMassageData = properties.containsKey("massage_start_time") || + properties.containsKey("head_type") || + properties.containsKey("body_part"); + + if (!hasMassageData) { + return; + } + + // 确保设备存在 + Device device = deviceManagerService.getOrCreateDevice(deviceId, null); + String normalizedDeviceId = device.getDeviceId(); + + // 检查是否重复按摩任务 + if (isDuplicateMassageTask(normalizedDeviceId, properties)) { + log.debug("重复的按摩任务,跳过处理: {}", normalizedDeviceId); + return; + } + + MassageTask task = createMassageTask(normalizedDeviceId, properties, device); + boolean saveResult = this.save(task); + + if (saveResult) { + log.info("按摩任务记录已保存: 设备{},按摩头{},部位{}", + normalizedDeviceId, task.getHeadType(), task.getBodyPart()); + } + + } catch (Exception e) { + log.error("处理按摩任务数据异常", e); + } + } + + /** + * 检查是否重复按摩任务 - 修复时间戳解析 + */ + private boolean isDuplicateMassageTask(String deviceId, Map properties) { + if (!properties.containsKey("massage_start_time") || properties.get("massage_start_time") == null) { + return false; + } + + try { + Object startTimeObj = properties.get("massage_start_time"); + Long startTimeMillis = parseTimestampToMillis(startTimeObj); + + if (startTimeMillis == null) { + log.warn("无法解析开始时间: {}", startTimeObj); + return false; + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(MassageTask::getDeviceId, deviceId) + .eq(MassageTask::getStartTime, startTimeMillis); + + if (properties.containsKey("head_type") && properties.get("head_type") != null) { + queryWrapper.eq(MassageTask::getHeadType, properties.get("head_type").toString()); + } + + return this.count(queryWrapper) > 0; + + } catch (Exception e) { + log.warn("检查重复按摩任务失败", e); + return false; + } + } + /** + * 创建按摩任务记录 - 修复版本 + */ + private MassageTask createMassageTask(String deviceId, Map properties, Device device) { + if (!properties.containsKey("massage_end_time")) { + return null; + } + + MassageTask task = new MassageTask(); + task.setDeviceId(deviceId); + task.setCreateTime(new Date()); + task.setCreateTimestamp(System.currentTimeMillis()); + + // 设置设备信息 + if (device != null) { + task.setDeviceName(device.getDeviceName()); + task.setProductName(device.getProductName()); + } else { + task.setDeviceName(deviceId); + task.setProductName("未知产品"); + } + + // 设置按摩头类型 + if (properties.containsKey("head_type")) { + task.setHeadType(properties.get("head_type").toString()); + } + + // 设置按摩部位 + if (properties.containsKey("body_part")) { + task.setBodyPart(properties.get("body_part").toString()); + } + + // 设置按摩方案 + if (properties.containsKey("massage_plan")) { + task.setMassagePlan(properties.get("massage_plan").toString()); + } + + // 安全处理时间 + Long startTime = safeParseTimestamp(properties.get("massage_start_time")); + Long endTime = safeParseTimestamp(properties.get("massage_end_time")); + + if (startTime != null) { + task.setStartTime(startTime); + task.setCreateTime(new Date(startTime)); + } + + if (endTime != null) { + task.setEndTime(endTime); + } + + // 安全计算任务时长 + Long duration = safeCalculateDuration(startTime, endTime); + if (duration != null) { + task.setTaskTime(duration); + log.debug("按摩任务时长计算: {} 秒", duration); + } else { + task.setTaskTime(0L); + log.warn("按摩任务时长计算失败,使用默认值0"); + } + + return task; + } + + /** + * 安全解析时间戳 - 修复版本 + */ + private Long safeParseTimestamp(Object timestampObj) { + if (timestampObj == null) return null; + + try { + long timestamp; + if (timestampObj instanceof Number) { + timestamp = ((Number) timestampObj).longValue(); + } else if (timestampObj instanceof String) { + String timestampStr = timestampObj.toString(); + // 处理科学计数法 + if (timestampStr.contains("E") || timestampStr.contains("e")) { + double doubleValue = Double.parseDouble(timestampStr); + timestamp = (long) doubleValue; + } else { + timestamp = Long.parseLong(timestampStr); + } + } else { + return null; + } + + // 验证时间戳的有效性 + if (!isValidTimestamp(timestamp)) { + log.warn("无效的时间戳: {}", timestamp); + return null; + } + + // 判断是秒级还是毫秒级 + if (timestamp < 10000000000L) { // 秒级时间戳(小于 2286-11-21) + return timestamp * 1000; + } else { // 毫秒级时间戳 + return timestamp; + } + } catch (Exception e) { + log.warn("时间戳解析失败: {}", timestampObj, e); + return null; + } + } + + /** + * 安全计算按摩持续时间 + */ + private Long safeCalculateDuration(Long startTime, Long endTime) { + if (startTime == null || endTime == null) { + return null; + } + + // 验证时间戳有效性 + if (!isValidTimestamp(startTime) || !isValidTimestamp(endTime)) { + log.warn("无效的时间戳,startTime: {}, endTime: {}", startTime, endTime); + return null; + } + + // 确保 startTime 和 endTime 都是毫秒级 + if (startTime < 10000000000L) { + startTime = startTime * 1000; + } + if (endTime < 10000000000L) { + endTime = endTime * 1000; + } + + // 检查时间顺序 + if (startTime >= endTime) { + log.warn("开始时间大于等于结束时间,startTime: {}, endTime: {}", startTime, endTime); + return null; + } + + // 检查持续时间是否合理(最长24小时) + long durationMs = endTime - startTime; + long maxValidDuration = 24 * 60 * 60 * 1000L; // 24小时 + + if (durationMs > maxValidDuration) { + log.warn("持续时间过长: {} 毫秒,超过最大限制 {}", durationMs, maxValidDuration); + return null; + } + + return durationMs / 1000; // 返回秒数 + } + + /** + * 验证时间戳是否有效 + */ + private boolean isValidTimestamp(long timestamp) { + // 检查是否为0或负数 + if (timestamp <= 0) { + return false; + } + + // 检查是否在合理范围内(1970年 - 2100年) + long minValidTimestamp = 0L; // 1970-01-01 + long maxValidTimestamp = 4102444800000L; // 2100-01-01 的毫秒时间戳 + + // 如果是秒级时间戳,转换为毫秒后检查 + if (timestamp < 10000000000L) { + timestamp = timestamp * 1000; + } + + return timestamp >= minValidTimestamp && timestamp <= maxValidTimestamp; + } + + /** + * 解析时间戳为毫秒(与DeviceStatusLogServiceImpl中的方法保持一致) + */ + private Long parseTimestampToMillis(Object timestampObj) { + if (timestampObj == null) return null; + + try { + if (timestampObj instanceof Number) { + long timestamp = ((Number) timestampObj).longValue(); + // 判断是秒级还是毫秒级时间戳 + if (timestamp < 10000000000L) { // 秒级时间戳 + return timestamp * 1000; + } else { // 毫秒级时间戳 + return timestamp; + } + } else if (timestampObj instanceof String) { + String timestampStr = timestampObj.toString(); + // 先尝试解析为数字 + try { + if (timestampStr.contains(".")) { + double timestamp = Double.parseDouble(timestampStr); + return (long) (timestamp * 1000); // 秒转毫秒 + } else { + long timestamp = Long.parseLong(timestampStr); + if (timestamp < 10000000000L) { + return timestamp * 1000; + } else { + return timestamp; + } + } + } catch (NumberFormatException e) { + // TODO 如果不是数字,尝试日期格式解析 + return null; + } + } + } catch (Exception e) { + log.error("时间戳解析失败: {}", timestampObj, e); + } + return null; + } +} diff --git a/storm-device/src/main/java/com/storm/device/task/DeviceDataSyncTask.java b/storm-device/src/main/java/com/storm/device/task/DeviceDataSyncTask.java new file mode 100644 index 0000000..14888a2 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/task/DeviceDataSyncTask.java @@ -0,0 +1,136 @@ +package com.storm.device.task; + +import com.storm.device.mapper.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.List; + +@Slf4j +@Component +public class DeviceDataSyncTask { + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper; + + @Autowired + private DeviceHeadUsageMapper deviceHeadUsageMapper; + + private void cleanupInvalidData() { + log.info("清理无效数据"); + // TODO + // 删除30天前的状态日志(保留最近数据) + // 删除无效的按摩任务记录等 + } + + /** + * 每天凌晨2点执行数据同步补偿 + */ + @Scheduled(cron = "0 0 2 * * ?") + @Transactional + public void syncDeviceData() { + log.info("开始执行设备数据同步补偿任务"); + + try { + // 记录开始时间 + long startTime = System.currentTimeMillis(); + + // 1. 同步设备总时长和统计表 + syncDeviceTotalDuration(); + + // 2. 同步按头使用统计 + syncHeadUsageStatistics(); + + // 3. 记录同步结果 + long endTime = System.currentTimeMillis(); + log.info("设备数据同步补偿任务完成,耗时: {}ms", (endTime - startTime)); + + } catch (Exception e) { + log.error("设备数据同步补偿任务执行失败", e); + } + } + + private void syncHeadUsageStatistics() { + log.info("开始同步按头使用统计"); + + try { + // 先删除旧数据(可选,根据需求调整) + String deleteSql = "DELETE FROM device_head_usage WHERE update_time < DATE_SUB(NOW(), INTERVAL 7 DAY)"; + deviceHeadUsageMapper.executeNativeSQL(deleteSql); + + // 插入/更新统计数据 + String syncSql = "INSERT INTO device_head_usage (device_id, head_type, usage_count, total_duration, " + + "daily_duration, weekly_duration, monthly_duration, last_used_time, create_time, update_time) " + + "SELECT device_id, head_type, COUNT(*) as usage_count, " + + "COALESCE(SUM(task_time), 0) as total_duration, " + + "COALESCE(SUM(CASE WHEN DATE(create_time) = CURDATE() THEN task_time ELSE 0 END), 0) as daily_duration, " + + "COALESCE(SUM(CASE WHEN create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN task_time ELSE 0 END), 0) as weekly_duration, " + + "COALESCE(SUM(CASE WHEN create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN task_time ELSE 0 END), 0) as monthly_duration, " + + "MAX(create_time) as last_used_time, NOW() as create_time, NOW() as update_time " + + "FROM massage_task " + + "WHERE create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) " + + "GROUP BY device_id, head_type " + + "ON DUPLICATE KEY UPDATE " + + "usage_count = VALUES(usage_count), " + + "total_duration = VALUES(total_duration), " + + "daily_duration = VALUES(daily_duration), " + + "weekly_duration = VALUES(weekly_duration), " + + "monthly_duration = VALUES(monthly_duration), " + + "last_used_time = VALUES(last_used_time), " + + "update_time = NOW()"; + + deviceHeadUsageMapper.executeNativeSQL(syncSql); + log.info("按头使用统计同步完成"); + + } catch (Exception e) { + log.error("同步按头使用统计失败", e); + } + } + + private void syncDeviceTotalDuration() { + log.info("开始同步设备总时长数据"); + + try { + // 修复SQL:使用device_runtime_stats表的总和来更新device表,避免重复累加 + String syncSql = "UPDATE device d " + + "SET total_online_duration = (" + + " SELECT COALESCE(SUM(daily_duration), 0) " + + " FROM device_runtime_stats s " + + " WHERE s.device_id = d.device_id" + + "), " + + "daily_duration = (" + + " SELECT COALESCE(SUM(daily_duration), 0) " + + " FROM device_runtime_stats s " + + " WHERE s.device_id = d.device_id AND s.stat_date = CURDATE()" + + "), " + + "weekly_duration = (" + + " SELECT COALESCE(SUM(daily_duration), 0) " + + " FROM device_runtime_stats s " + + " WHERE s.device_id = d.device_id AND s.stat_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)" + + "), " + + "monthly_duration = (" + + " SELECT COALESCE(SUM(daily_duration), 0) " + + " FROM device_runtime_stats s " + + " WHERE s.device_id = d.device_id AND s.stat_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)" + + "), " + + "update_time = NOW() " + + "WHERE EXISTS (" + + " SELECT 1 FROM device_runtime_stats s " + + " WHERE s.device_id = d.device_id" + + ")"; + + deviceMapper.executeNativeSQL(syncSql); + log.info("设备总时长数据同步完成"); + + } catch (Exception e) { + log.error("同步设备总时长数据失败", e); + } + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/task/LocationAmqpClient.java b/storm-device/src/main/java/com/storm/device/task/LocationAmqpClient.java new file mode 100644 index 0000000..c339ab6 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/task/LocationAmqpClient.java @@ -0,0 +1,265 @@ +package com.storm.device.task; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.storm.device.config.properties.HuaWeiIotConfigProperties; +import com.storm.device.processor.DeviceDataProcessor; +import com.storm.device.service.IDeviceService; +import com.storm.device.service.IDeviceStatusLogService; +import com.storm.device.service.IMassageTaskService; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import lombok.extern.slf4j.Slf4j; +import org.apache.qpid.jms.*; +import org.apache.qpid.jms.message.JmsInboundMessageDispatch; +import org.apache.qpid.jms.transports.TransportOptions; +import org.apache.qpid.jms.transports.TransportSupport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.jms.*; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + + +@Slf4j +@Component +public class LocationAmqpClient implements ApplicationRunner { + @Autowired + private DeviceDataProcessor deviceDataProcessor; + @Autowired + private HuaWeiIotConfigProperties huaWeiIotConfigProperties; + + //业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。 + @Autowired + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。 + //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。 + private static String clientId; + + @Resource + private IDeviceStatusLogService deviceDataService; + + @Resource + private IMassageTaskService massageTaskService; + + @Resource + private IDeviceService deviceService; + + static { + try { + clientId = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + } + + public void run(ApplicationArguments args) throws Exception { + start(); + } + + public void start() throws Exception { + //参数说明,请参见AMQP客户端接入说明文档。 + for (int i = 0; i < huaWeiIotConfigProperties.getConnectionCount(); i++) { + //创建amqp连接 + Connection connection = getConnection(); + + //加入监听者 + ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener); + // 创建会话。 + // Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。 + // Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。 + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + connection.start(); + + // 创建Receiver连接。 + MessageConsumer consumer = newConsumer(session, connection, "queue-location"); + consumer.setMessageListener(messageListener); + } + + log.info("amqp is started successfully, and will exit after server shutdown "); + } + + /** + * 创建amqp连接 + * + * @return amqp连接 + */ + private Connection getConnection() throws Exception { + String connectionUrl = generateConnectUrl(); + JmsConnectionFactory cf = new JmsConnectionFactory(connectionUrl); + // 信任服务端 + TransportOptions to = new TransportOptions(); + to.setTrustAll(true); + cf.setSslContext(TransportSupport.createJdkSslContext(to)); + String userName = "accessKey=" + huaWeiIotConfigProperties.getAccessKey(); + cf.setExtension(JmsConnectionExtensions.USERNAME_OVERRIDE.toString(), (connection, uri) -> { + // IoTDA的userName组成格式如下:“accessKey=${accessKey}|timestamp=${timestamp}” + String newUserName = userName; + if (connection instanceof JmsConnection) { + newUserName = ((JmsConnection) connection).getUsername(); + } + return newUserName + "|timestamp=" + System.currentTimeMillis(); + }); + + // 创建连接。 + return cf.createConnection(userName, huaWeiIotConfigProperties.getAccessCode()); + } + + /** + * 生成amqp连接地址 + * + * @return amqp连接地址 + */ + public String generateConnectUrl() { + String uri = MessageFormat.format("{0}://{1}:{2}", + (huaWeiIotConfigProperties.isUseSsl() ? "amqps" : "amqp"), + huaWeiIotConfigProperties.getHost(), + String.valueOf(huaWeiIotConfigProperties.getPort())); + Map uriOptions = new HashMap<>(); + uriOptions.put("amqp.vhost", huaWeiIotConfigProperties.getVhost()); + uriOptions.put("amqp.idleTimeout", String.valueOf(huaWeiIotConfigProperties.getIdleTimeout())); + uriOptions.put("amqp.saslMechanisms", huaWeiIotConfigProperties.getSaslMechanisms()); + + Map jmsOptions = new HashMap<>(); + jmsOptions.put("jms.prefetchPolicy.queuePrefetch", String.valueOf(huaWeiIotConfigProperties.getQueuePrefetch())); + if (CharSequenceUtil.isNotBlank(clientId)) { + jmsOptions.put("jms.clientID", clientId); + } else { + jmsOptions.put("jms.clientID", UUID.randomUUID().toString()); + } + jmsOptions.put("failover.reconnectDelay", String.valueOf(huaWeiIotConfigProperties.getReconnectDelay())); + jmsOptions.put("failover.maxReconnectDelay", String.valueOf(huaWeiIotConfigProperties.getMaxReconnectDelay())); + if (huaWeiIotConfigProperties.getMaxReconnectAttempts() > 0) { + jmsOptions.put("failover.maxReconnectAttempts", String.valueOf(huaWeiIotConfigProperties.getMaxReconnectAttempts())); + } + if (huaWeiIotConfigProperties.getExtendedOptions() != null) { + for (Map.Entry option : huaWeiIotConfigProperties.getExtendedOptions().entrySet()) { + if (option.getKey().startsWith("amqp.") || option.getKey().startsWith("transport.")) { + uriOptions.put(option.getKey(), option.getValue()); + } else { + jmsOptions.put(option.getKey(), option.getValue()); + } + } + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(uriOptions.entrySet().stream() + .map(option -> MessageFormat.format("{0}={1}", option.getKey(), option.getValue())) + .collect(Collectors.joining("&", "failover:(" + uri + "?", ")"))); + stringBuilder.append(jmsOptions.entrySet().stream() + .map(option -> MessageFormat.format("{0}={1}", option.getKey(), option.getValue())) + .collect(Collectors.joining("&", "?", ""))); + return stringBuilder.toString(); + } + + /** + * 创建消费者 + * + * @param session session + * @param connection amqp连接 + * @param queueName 队列名称 + * @return 消费者 + */ + public MessageConsumer newConsumer(Session session, Connection connection, String queueName) throws Exception { + if (connection == null || !(connection instanceof JmsConnection) || ((JmsConnection) connection).isClosed()) { + throw new Exception("create consumer failed,the connection is disconnected."); + } + + return session.createConsumer(new JmsQueue(queueName)); + } + + private final MessageListener messageListener = message -> { + try { + //异步处理收到的消息,确保onMessage函数里没有耗时逻辑 + threadPoolTaskExecutor.submit(() -> processMessage(message)); + } catch (Exception e) { + log.error("submit task occurs exception ", e); + } + }; + + // 修改processMessage方法 + private void processMessage(Message message) { + try { + String contentStr = message.getBody(String.class); + String topic = message.getStringProperty("topic"); + String messageId = message.getStringProperty("messageId"); + log.info("receive message,\n topic = {},\n messageId = {},\n content = {}", topic, messageId, contentStr); + + JSONObject jsonMsg = JSONUtil.parseObj(contentStr); + JSONObject notifyData = jsonMsg.getJSONObject("notify_data"); + if (ObjectUtil.isEmpty(notifyData)) { + return; + } + + IotMsgNotifyDataPro iotMsgNotifyData = JSONUtil.toBean(notifyData, IotMsgNotifyDataPro.class); + + // 使用统一处理器处理所有数据 + deviceDataProcessor.processDeviceData(iotMsgNotifyData); + + } catch (Exception e) { + log.error("处理消息时发生异常", e); + } + } + + private final JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() { + /** + * 连接成功建立。 + */ + @Override + public void onConnectionEstablished(URI remoteURI) { + log.info("onConnectionEstablished, remoteUri:{}", remoteURI); + } + + /** + * 尝试过最大重试次数之后,最终连接失败。 + */ + @Override + public void onConnectionFailure(Throwable error) { + log.error("onConnectionFailure, {}", error.getMessage()); + } + + /** + * 连接中断。 + */ + @Override + public void onConnectionInterrupted(URI remoteURI) { + log.info("onConnectionInterrupted, remoteUri:{}", remoteURI); + } + + /** + * 连接中断后又自动重连上。 + */ + @Override + public void onConnectionRestored(URI remoteURI) { + log.info("onConnectionRestored, remoteUri:{}", remoteURI); + } + + @Override + public void onInboundMessage(JmsInboundMessageDispatch envelope) { + } + + @Override + public void onSessionClosed(Session session, Throwable cause) { + } + + @Override + public void onConsumerClosed(MessageConsumer consumer, Throwable cause) { + } + + @Override + public void onProducerClosed(MessageProducer producer, Throwable cause) { + } + }; +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/task/MassageAmqpClient.java b/storm-device/src/main/java/com/storm/device/task/MassageAmqpClient.java new file mode 100644 index 0000000..f925404 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/task/MassageAmqpClient.java @@ -0,0 +1,262 @@ +package com.storm.device.task; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.storm.device.config.properties.HuaWeiIotConfigProperties; +import com.storm.device.processor.DeviceDataProcessor; +import com.storm.device.service.IDeviceService; +import com.storm.device.service.IDeviceStatusLogService; +import com.storm.device.service.IMassageTaskService; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import lombok.extern.slf4j.Slf4j; +import org.apache.qpid.jms.*; +import org.apache.qpid.jms.message.JmsInboundMessageDispatch; +import org.apache.qpid.jms.transports.TransportOptions; +import org.apache.qpid.jms.transports.TransportSupport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import javax.annotation.Resource; +import javax.jms.*; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class MassageAmqpClient implements ApplicationRunner { + @Autowired + private DeviceDataProcessor deviceDataProcessor; + @Autowired + private HuaWeiIotConfigProperties huaWeiIotConfigProperties; + + @Autowired + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。 + //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。 + private static String clientId; + + @Resource + private IDeviceStatusLogService deviceDataService; + + @Resource + private IMassageTaskService massageTaskService; + + @Resource + private IDeviceService deviceService; + + static { + try { + clientId = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + } + + public void run(ApplicationArguments args) throws Exception { + start(); + } + + public void start() throws Exception { + //参数说明,请参见AMQP客户端接入说明文档。 + for (int i = 0; i < huaWeiIotConfigProperties.getConnectionCount(); i++) { + //创建amqp连接 + Connection connection = getConnection(); + + //加入监听者 + ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener); + // 创建会话。 + // Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。 + // Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。 + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + connection.start(); + + // 创建Receiver连接。 + MessageConsumer consumer = newConsumer(session, connection, "queue-massage"); + consumer.setMessageListener(messageListener); + } + + log.info("amqp is started successfully, and will exit after server shutdown "); + } + + /** + * 创建amqp连接 + * + * @return amqp连接 + */ + private Connection getConnection() throws Exception { + String connectionUrl = generateConnectUrl(); + JmsConnectionFactory cf = new JmsConnectionFactory(connectionUrl); + // 信任服务端 + TransportOptions to = new TransportOptions(); + to.setTrustAll(true); + cf.setSslContext(TransportSupport.createJdkSslContext(to)); + String userName = "accessKey=" + huaWeiIotConfigProperties.getAccessKey(); + cf.setExtension(JmsConnectionExtensions.USERNAME_OVERRIDE.toString(), (connection, uri) -> { + // IoTDA的userName组成格式如下:“accessKey=${accessKey}|timestamp=${timestamp}” + String newUserName = userName; + if (connection instanceof JmsConnection) { + newUserName = ((JmsConnection) connection).getUsername(); + } + return newUserName + "|timestamp=" + System.currentTimeMillis(); + }); + + // 创建连接。 + return cf.createConnection(userName, huaWeiIotConfigProperties.getAccessCode()); + } + + /** + * 生成amqp连接地址 + * + * @return amqp连接地址 + */ + public String generateConnectUrl() { + String uri = MessageFormat.format("{0}://{1}:{2}", + (huaWeiIotConfigProperties.isUseSsl() ? "amqps" : "amqp"), + huaWeiIotConfigProperties.getHost(), + String.valueOf(huaWeiIotConfigProperties.getPort())); + Map uriOptions = new HashMap<>(); + uriOptions.put("amqp.vhost", huaWeiIotConfigProperties.getVhost()); + uriOptions.put("amqp.idleTimeout", String.valueOf(huaWeiIotConfigProperties.getIdleTimeout())); + uriOptions.put("amqp.saslMechanisms", huaWeiIotConfigProperties.getSaslMechanisms()); + + Map jmsOptions = new HashMap<>(); + jmsOptions.put("jms.prefetchPolicy.queuePrefetch", String.valueOf(huaWeiIotConfigProperties.getQueuePrefetch())); + if (CharSequenceUtil.isNotBlank(clientId)) { + jmsOptions.put("jms.clientID", clientId); + } else { + jmsOptions.put("jms.clientID", UUID.randomUUID().toString()); + } + jmsOptions.put("failover.reconnectDelay", String.valueOf(huaWeiIotConfigProperties.getReconnectDelay())); + jmsOptions.put("failover.maxReconnectDelay", String.valueOf(huaWeiIotConfigProperties.getMaxReconnectDelay())); + if (huaWeiIotConfigProperties.getMaxReconnectAttempts() > 0) { + jmsOptions.put("failover.maxReconnectAttempts", String.valueOf(huaWeiIotConfigProperties.getMaxReconnectAttempts())); + } + if (huaWeiIotConfigProperties.getExtendedOptions() != null) { + for (Map.Entry option : huaWeiIotConfigProperties.getExtendedOptions().entrySet()) { + if (option.getKey().startsWith("amqp.") || option.getKey().startsWith("transport.")) { + uriOptions.put(option.getKey(), option.getValue()); + } else { + jmsOptions.put(option.getKey(), option.getValue()); + } + } + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(uriOptions.entrySet().stream() + .map(option -> MessageFormat.format("{0}={1}", option.getKey(), option.getValue())) + .collect(Collectors.joining("&", "failover:(" + uri + "?", ")"))); + stringBuilder.append(jmsOptions.entrySet().stream() + .map(option -> MessageFormat.format("{0}={1}", option.getKey(), option.getValue())) + .collect(Collectors.joining("&", "?", ""))); + return stringBuilder.toString(); + } + + /** + * 创建消费者 + * + * @param session session + * @param connection amqp连接 + * @param queueName 队列名称 + * @return 消费者 + */ + public MessageConsumer newConsumer(Session session, Connection connection, String queueName) throws Exception { + if (connection == null || !(connection instanceof JmsConnection) || ((JmsConnection) connection).isClosed()) { + throw new Exception("create consumer failed,the connection is disconnected."); + } + + return session.createConsumer(new JmsQueue(queueName)); + } + + private final MessageListener messageListener = message -> { + try { + //异步处理收到的消息,确保onMessage函数里没有耗时逻辑 + threadPoolTaskExecutor.submit(() -> processMessage(message)); + } catch (Exception e) { + log.error("submit task occurs exception ", e); + } + }; + + // 修改processMessage方法 + private void processMessage(Message message) { + try { + String contentStr = message.getBody(String.class); + String topic = message.getStringProperty("topic"); + String messageId = message.getStringProperty("messageId"); + log.info("receive message,\n topic = {},\n messageId = {},\n content = {}", topic, messageId, contentStr); + + JSONObject jsonMsg = JSONUtil.parseObj(contentStr); + JSONObject notifyData = jsonMsg.getJSONObject("notify_data"); + if (ObjectUtil.isEmpty(notifyData)) { + return; + } + + IotMsgNotifyDataPro iotMsgNotifyData = JSONUtil.toBean(notifyData, IotMsgNotifyDataPro.class); + + // 使用统一处理器处理所有数据 + deviceDataProcessor.processDeviceData(iotMsgNotifyData); + + } catch (Exception e) { + log.error("处理消息时发生异常", e); + } + } + + private final JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() { + /** + * 连接成功建立。 + */ + @Override + public void onConnectionEstablished(URI remoteURI) { + log.info("onConnectionEstablished, remoteUri:{}", remoteURI); + } + + /** + * 尝试过最大重试次数之后,最终连接失败。 + */ + @Override + public void onConnectionFailure(Throwable error) { + log.error("onConnectionFailure, {}", error.getMessage()); + } + + /** + * 连接中断。 + */ + @Override + public void onConnectionInterrupted(URI remoteURI) { + log.info("onConnectionInterrupted, remoteUri:{}", remoteURI); + } + + /** + * 连接中断后又自动重连上。 + */ + @Override + public void onConnectionRestored(URI remoteURI) { + log.info("onConnectionRestored, remoteUri:{}", remoteURI); + } + + @Override + public void onInboundMessage(JmsInboundMessageDispatch envelope) { + } + + @Override + public void onSessionClosed(Session session, Throwable cause) { + } + + @Override + public void onConsumerClosed(MessageConsumer consumer, Throwable cause) { + } + + @Override + public void onProducerClosed(MessageProducer producer, Throwable cause) { + } + }; +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/task/StatusAmqpClient.java b/storm-device/src/main/java/com/storm/device/task/StatusAmqpClient.java new file mode 100644 index 0000000..020cf02 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/task/StatusAmqpClient.java @@ -0,0 +1,485 @@ +package com.storm.device.task; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.storm.device.config.properties.HuaWeiIotConfigProperties; +import com.storm.device.domain.po.Device; +import com.storm.device.mapper.DeviceMapper; +import com.storm.device.processor.DeviceDataProcessor; +import com.storm.device.service.IDeviceService; +import com.storm.device.service.IDeviceStatusLogService; +import com.storm.device.service.IMassageTaskService; +import com.storm.device.task.vo.IotMsgNotifyDataPro; +import lombok.extern.slf4j.Slf4j; +import org.apache.qpid.jms.*; +import org.apache.qpid.jms.message.JmsInboundMessageDispatch; +import org.apache.qpid.jms.transports.TransportOptions; +import org.apache.qpid.jms.transports.TransportSupport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.jms.*; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + + +@Slf4j +@Component +public class StatusAmqpClient implements ApplicationRunner { + @Autowired + private DeviceDataProcessor deviceDataProcessor; + @Autowired + private HuaWeiIotConfigProperties huaWeiIotConfigProperties; + + //业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。 + @Autowired + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。 + //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。 + private static String clientId; + + @Resource + private IDeviceStatusLogService deviceDataService; + + @Resource + private IMassageTaskService massageTaskService; + + @Resource + private IDeviceService deviceService; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private DeviceMapper deviceMapper; + + static { + try { + clientId = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + } + + public void run(ApplicationArguments args) throws Exception { + start(); + } + + public void start() throws Exception { + //参数说明,请参见AMQP客户端接入说明文档。 + for (int i = 0; i < huaWeiIotConfigProperties.getConnectionCount(); i++) { + //创建amqp连接 + Connection connection = getConnection(); + + //加入监听者 + ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener); + // 创建会话。 + // Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。 + // Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。 + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + connection.start(); + + // 创建Receiver连接。 + MessageConsumer consumer = newConsumer(session, connection, "queue-status"); + consumer.setMessageListener(messageListener); + } + + log.info("amqp is started successfully, and will exit after server shutdown "); + } + + /** + * 创建amqp连接 + * + * @return amqp连接 + */ + private Connection getConnection() throws Exception { + String connectionUrl = generateConnectUrl(); + JmsConnectionFactory cf = new JmsConnectionFactory(connectionUrl); + // 信任服务端 + TransportOptions to = new TransportOptions(); + to.setTrustAll(true); + cf.setSslContext(TransportSupport.createJdkSslContext(to)); + String userName = "accessKey=" + huaWeiIotConfigProperties.getAccessKey(); + cf.setExtension(JmsConnectionExtensions.USERNAME_OVERRIDE.toString(), (connection, uri) -> { + // IoTDA的userName组成格式如下:“accessKey=${accessKey}|timestamp=${timestamp}” + String newUserName = userName; + if (connection instanceof JmsConnection) { + newUserName = ((JmsConnection) connection).getUsername(); + } + return newUserName + "|timestamp=" + System.currentTimeMillis(); + }); + + // 创建连接。 + return cf.createConnection(userName, huaWeiIotConfigProperties.getAccessCode()); + } + + /** + * 生成amqp连接地址 + * + * @return amqp连接地址 + */ + public String generateConnectUrl() { + String uri = MessageFormat.format("{0}://{1}:{2}", + (huaWeiIotConfigProperties.isUseSsl() ? "amqps" : "amqp"), + huaWeiIotConfigProperties.getHost(), + String.valueOf(huaWeiIotConfigProperties.getPort())); + Map uriOptions = new HashMap<>(); + uriOptions.put("amqp.vhost", huaWeiIotConfigProperties.getVhost()); + uriOptions.put("amqp.idleTimeout", String.valueOf(huaWeiIotConfigProperties.getIdleTimeout())); + uriOptions.put("amqp.saslMechanisms", huaWeiIotConfigProperties.getSaslMechanisms()); + + Map jmsOptions = new HashMap<>(); + jmsOptions.put("jms.prefetchPolicy.queuePrefetch", String.valueOf(huaWeiIotConfigProperties.getQueuePrefetch())); + if (CharSequenceUtil.isNotBlank(clientId)) { + jmsOptions.put("jms.clientID", clientId); + } else { + jmsOptions.put("jms.clientID", UUID.randomUUID().toString()); + } + jmsOptions.put("failover.reconnectDelay", String.valueOf(huaWeiIotConfigProperties.getReconnectDelay())); + jmsOptions.put("failover.maxReconnectDelay", String.valueOf(huaWeiIotConfigProperties.getMaxReconnectDelay())); + if (huaWeiIotConfigProperties.getMaxReconnectAttempts() > 0) { + jmsOptions.put("failover.maxReconnectAttempts", String.valueOf(huaWeiIotConfigProperties.getMaxReconnectAttempts())); + } + if (huaWeiIotConfigProperties.getExtendedOptions() != null) { + for (Map.Entry option : huaWeiIotConfigProperties.getExtendedOptions().entrySet()) { + if (option.getKey().startsWith("amqp.") || option.getKey().startsWith("transport.")) { + uriOptions.put(option.getKey(), option.getValue()); + } else { + jmsOptions.put(option.getKey(), option.getValue()); + } + } + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(uriOptions.entrySet().stream() + .map(option -> MessageFormat.format("{0}={1}", option.getKey(), option.getValue())) + .collect(Collectors.joining("&", "failover:(" + uri + "?", ")"))); + stringBuilder.append(jmsOptions.entrySet().stream() + .map(option -> MessageFormat.format("{0}={1}", option.getKey(), option.getValue())) + .collect(Collectors.joining("&", "?", ""))); + return stringBuilder.toString(); + } + + /** + * 创建消费者 + * + * @param session session + * @param connection amqp连接 + * @param queueName 队列名称 + * @return 消费者 + */ + public MessageConsumer newConsumer(Session session, Connection connection, String queueName) throws Exception { + if (connection == null || !(connection instanceof JmsConnection) || ((JmsConnection) connection).isClosed()) { + throw new Exception("create consumer failed,the connection is disconnected."); + } + + return session.createConsumer(new JmsQueue(queueName)); + } + + private final MessageListener messageListener = message -> { + try { + //异步处理收到的消息,确保onMessage函数里没有耗时逻辑 + threadPoolTaskExecutor.submit(() -> processMessage(message)); + } catch (Exception e) { + log.error("submit task occurs exception ", e); + } + }; + + // 修改processMessage方法 + private void processMessage(Message message) { + try { + String contentStr = message.getBody(String.class); + String topic = message.getStringProperty("topic"); + String messageId = message.getStringProperty("messageId"); + log.info("receive message,\n topic = {},\n messageId = {},\n content = {}", topic, messageId, contentStr); + + JSONObject jsonMsg = JSONUtil.parseObj(contentStr); + JSONObject notifyData = jsonMsg.getJSONObject("notify_data"); + if (ObjectUtil.isEmpty(notifyData)) { + return; + } + + // 手动处理日期字段,避免Hutool自动转换失败 + IotMsgNotifyDataPro iotMsgNotifyData = parseIotMsgNotifyData(notifyData); + + // 使用统一处理器处理所有数据 + deviceDataProcessor.processDeviceData(iotMsgNotifyData); + + } catch (Exception e) { + log.error("处理消息时发生异常", e); + } + } + + /** + * 手动解析IoT消息数据,处理日期格式问题 + */ + private IotMsgNotifyDataPro parseIotMsgNotifyData(JSONObject notifyData) { + IotMsgNotifyDataPro iotMsgNotifyData = new IotMsgNotifyDataPro(); + + try { + // 解析header + JSONObject header = notifyData.getJSONObject("header"); + if (header != null) { + IotMsgNotifyDataPro.IotMsgHeader msgHeader = new IotMsgNotifyDataPro.IotMsgHeader(); + msgHeader.setAppId(header.getStr("app_id")); + msgHeader.setDeviceId(header.getStr("device_id")); + msgHeader.setNodeId(header.getStr("node_id")); + msgHeader.setProductId(header.getStr("product_id")); + msgHeader.setGatewayId(header.getStr("gateway_id")); + iotMsgNotifyData.setHeader(msgHeader); + } + + // 解析body + JSONObject body = notifyData.getJSONObject("body"); + if (body != null) { + IotMsgNotifyDataPro.IotMsgBody msgBody = new IotMsgNotifyDataPro.IotMsgBody(); + msgBody.setStatus(body.getStr("status")); + + // 手动处理日期字段 + String statusUpdateTimeStr = body.getStr("status_update_time"); + if (StrUtil.isNotBlank(statusUpdateTimeStr)) { + try { + Date statusUpdateTime = parseIsoDateTime(statusUpdateTimeStr); + msgBody.setStatusUpdateTime(statusUpdateTime); + } catch (Exception e) { + log.warn("解析status_update_time失败: {}", statusUpdateTimeStr); + } + } + + String lastOnlineTimeStr = body.getStr("last_online_time"); + if (StrUtil.isNotBlank(lastOnlineTimeStr)) { + try { + Date lastOnlineTime = parseIsoDateTime(lastOnlineTimeStr); + msgBody.setLastOnlineTime(lastOnlineTime); + } catch (Exception e) { + log.warn("解析last_online_time失败: {}", lastOnlineTimeStr); + } + } + + // 解析services + JSONArray servicesArray = body.getJSONArray("services"); + if (servicesArray != null) { + List services = new ArrayList<>(); + for (int i = 0; i < servicesArray.size(); i++) { + JSONObject serviceObj = servicesArray.getJSONObject(i); + IotMsgNotifyDataPro.IotMsgService service = new IotMsgNotifyDataPro.IotMsgService(); + service.setServiceId(serviceObj.getStr("service_id")); + + // 处理event_time + String eventTimeStr = serviceObj.getStr("event_time"); + if (StrUtil.isNotBlank(eventTimeStr)) { + try { + Date eventTime = parseIsoDateTime(eventTimeStr); + service.setEventTime(eventTime); + } catch (Exception e) { + log.warn("解析event_time失败: {}", eventTimeStr); + } + } + + // 处理properties + JSONObject propertiesObj = serviceObj.getJSONObject("properties"); + if (propertiesObj != null) { + Map properties = new HashMap<>(); + for (String key : propertiesObj.keySet()) { + properties.put(key, propertiesObj.get(key)); + } + service.setProperties(properties); + } + + services.add(service); + } + msgBody.setServices(services); + } + + iotMsgNotifyData.setBody(msgBody); + } + + } catch (Exception e) { + log.error("手动解析IoT消息数据失败", e); + // 如果手动解析失败,回退到Hutool的自动解析(可能会失败) + try { + return JSONUtil.toBean(notifyData, IotMsgNotifyDataPro.class); + } catch (Exception ex) { + log.error("Hutool自动解析也失败", ex); + } + } + + return iotMsgNotifyData; + } + + /** + * 实时更新设备状态 + */ + private void updateRealtimeDeviceStatus(IotMsgNotifyDataPro iotMsgNotifyData) { + String deviceId = iotMsgNotifyData.getHeader().getDeviceId(); + String normalizedDeviceId = deviceId.replaceAll(".*_", ""); + + // 使用Redis实现实时状态缓存 + String statusKey = "device:status:" + normalizedDeviceId; + String lastOnlineKey = "device:last_online:" + normalizedDeviceId; + + if (iotMsgNotifyData.getBody() != null) { + String status = iotMsgNotifyData.getBody().getStatus(); + + // 更新Redis缓存 + redisTemplate.opsForValue().set(statusKey, status, Duration.ofMinutes(10)); + + if ("ONLINE".equals(status)) { + // 设备上线,记录最后上线时间 + redisTemplate.opsForValue().set(lastOnlineKey, + LocalDateTime.now().toString(), Duration.ofDays(30)); + + // 更新数据库 + deviceMapper.update(null, + Wrappers.lambdaUpdate(Device.class) + .set(Device::getStatus, "ONLINE") + .set(Device::getLastOnlineTime, new Date()) + .eq(Device::getDeviceId, normalizedDeviceId)); + + } else if ("OFFLINE".equals(status)) { + // 设备下线 + deviceMapper.update(null, + Wrappers.lambdaUpdate(Device.class) + .set(Device::getStatus, "OFFLINE") + .set(Device::getLastOfflineTime, new Date()) + .eq(Device::getDeviceId, normalizedDeviceId)); + } + } + } + + // 在 StatusAmqpClient 中修复日期解析方法 + /** + * 解析ISO日期时间格式(增强版) + */ + private Date parseIsoDateTime(String dateTimeStr) { + if (StrUtil.isBlank(dateTimeStr)) { + return null; + } + + try { + // 处理格式:20250927T070832Z (16位,没有分隔符) + if (dateTimeStr.length() == 16 && dateTimeStr.endsWith("Z") && dateTimeStr.contains("T")) { + String pattern = "yyyyMMdd'T'HHmmss'Z'"; + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + Date utcDate = sdf.parse(dateTimeStr); + + // 转换为本地时间(如果需要) + return utcDate; + } + + // 处理格式:2025-09-27T07:08:31.394Z (标准ISO格式) + if (dateTimeStr.length() == 24 && dateTimeStr.contains(".")) { + String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + return sdf.parse(dateTimeStr); + } + + // 处理格式:2025-09-27T07:08:31Z (没有毫秒) + if (dateTimeStr.length() == 20 && dateTimeStr.endsWith("Z")) { + String pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + return sdf.parse(dateTimeStr); + } + + // 尝试Hutool的自动解析 + return DateUtil.parse(dateTimeStr); + + } catch (Exception e) { + log.warn("日期解析失败: {}, 尝试其他格式", dateTimeStr); + + // 尝试其他常见格式 + String[] patterns = { + "yyyyMMdd'T'HHmmss'Z'", // 20250927T070832Z + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", // 2025-09-27T07:08:31.394Z + "yyyy-MM-dd'T'HH:mm:ss'Z'", // 2025-09-27T07:08:31Z + "yyyyMMddHHmmss", // 20250927070832 + "yyyy-MM-dd HH:mm:ss" // 2025-09-27 07:08:32 + }; + + for (String pattern : patterns) { + try { + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + return sdf.parse(dateTimeStr); + } catch (Exception ex) { + // 继续尝试下一个格式 + continue; + } + } + + log.error("所有日期格式解析都失败: {}", dateTimeStr); + return new Date(); // 返回当前时间作为兜底 + } + } + + private final JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() { + /** + * 连接成功建立。 + */ + @Override + public void onConnectionEstablished(URI remoteURI) { + log.info("onConnectionEstablished, remoteUri:{}", remoteURI); + } + + /** + * 尝试过最大重试次数之后,最终连接失败。 + */ + @Override + public void onConnectionFailure(Throwable error) { + log.error("onConnectionFailure, {}", error.getMessage()); + } + + /** + * 连接中断。 + */ + @Override + public void onConnectionInterrupted(URI remoteURI) { + log.info("onConnectionInterrupted, remoteUri:{}", remoteURI); + } + + /** + * 连接中断后又自动重连上。 + */ + @Override + public void onConnectionRestored(URI remoteURI) { + log.info("onConnectionRestored, remoteUri:{}", remoteURI); + } + + @Override + public void onInboundMessage(JmsInboundMessageDispatch envelope) { + } + + @Override + public void onSessionClosed(Session session, Throwable cause) { + } + + @Override + public void onConsumerClosed(MessageConsumer consumer, Throwable cause) { + } + + @Override + public void onProducerClosed(MessageProducer producer, Throwable cause) { + } + }; +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/task/vo/DeviceLocationVO.java b/storm-device/src/main/java/com/storm/device/task/vo/DeviceLocationVO.java new file mode 100644 index 0000000..03e8c7c --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/task/vo/DeviceLocationVO.java @@ -0,0 +1,17 @@ +// 设备位置信息VO(用于MongoDB存储) +package com.storm.device.task.vo; + +import lombok.Data; +import java.util.Date; + +@Data +public class DeviceLocationVO { + private String deviceId; + private Double longitude; + private Double latitude; + private String province; + private String city; + private String weather; + private Date timestamp; + private Date createTime; +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/task/vo/IotMsgBody.java b/storm-device/src/main/java/com/storm/device/task/vo/IotMsgBody.java new file mode 100644 index 0000000..b676bf6 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/task/vo/IotMsgBody.java @@ -0,0 +1,29 @@ +package com.storm.device.task.vo; + +import lombok.Data; + +import java.util.List; + + +@Data +public class IotMsgBody { + /** + * 服务列表 + */ + private List services; + + /** + * 最后在线时间 + */ + private String lastOnlineTime; + + /** + * 在线状态 + */ + private String status; + + /** + * 更新时间 + */ + private String statusUpdateTime; +} diff --git a/storm-device/src/main/java/com/storm/device/task/vo/IotMsgNotifyDataPro.java b/storm-device/src/main/java/com/storm/device/task/vo/IotMsgNotifyDataPro.java new file mode 100644 index 0000000..a780ef9 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/task/vo/IotMsgNotifyDataPro.java @@ -0,0 +1,42 @@ +// IotMsgNotifyDataPro.java - 根据日志调整结构 +package com.storm.device.task.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Data +public class IotMsgNotifyDataPro { + private IotMsgHeader header; + private IotMsgBody body; + + @Data + public static class IotMsgHeader { + private String productId; + private String deviceId; + private String nodeId; + private String appId; + private String gatewayId; + } + + @Data + public static class IotMsgBody { + private List services; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date lastOnlineTime; + private String status; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date statusUpdateTime; + } + + @Data + public static class IotMsgService { + private String serviceId; + private Map properties; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date eventTime; + } +} \ No newline at end of file diff --git a/storm-device/src/main/java/com/storm/device/task/vo/IotMsgService.java b/storm-device/src/main/java/com/storm/device/task/vo/IotMsgService.java new file mode 100644 index 0000000..9546350 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/task/vo/IotMsgService.java @@ -0,0 +1,24 @@ +package com.storm.device.task.vo; + +import lombok.Data; + +import java.util.Map; + + +@Data +public class IotMsgService { + /** + * 服务id + */ + private String serviceId; + + /** + * 设备上报属性 + */ + private Map properties; + + /** + * 时间,格式:yyyyMMdd'T'HHmmss'Z' + */ + private String eventTime; +} diff --git a/storm-device/src/main/java/com/storm/device/webSocket/DeviceStatusWebSocket.java b/storm-device/src/main/java/com/storm/device/webSocket/DeviceStatusWebSocket.java new file mode 100644 index 0000000..4c7a025 --- /dev/null +++ b/storm-device/src/main/java/com/storm/device/webSocket/DeviceStatusWebSocket.java @@ -0,0 +1,44 @@ +package com.storm.device.webSocket; + +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.websocket.OnClose; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +// WebSocket服务端,实现实时状态推送 +@ServerEndpoint("/websocket/device-status") +@Component +@Slf4j +public class DeviceStatusWebSocket { + + private static ConcurrentHashMap sessions = new ConcurrentHashMap<>(); + + @OnOpen + public void onOpen(Session session, @PathParam("deviceId") String deviceId) { + sessions.put(deviceId, session); + // 推送当前状态 + } + + @OnClose + public void onClose(@PathParam("deviceId") String deviceId) { + sessions.remove(deviceId); + } + + public static void pushStatusUpdate(String deviceId, Map status) { + Session session = sessions.get(deviceId); + if (session != null && session.isOpen()) { + try { + session.getBasicRemote().sendText(JSONUtil.toJsonStr(status)); + } catch (Exception e) { + log.error("WebSocket推送失败", e); + } + } + } +} \ No newline at end of file diff --git a/storm-device/src/main/resources/appliation.yml b/storm-device/src/main/resources/appliation.yml new file mode 100644 index 0000000..28f59b2 --- /dev/null +++ b/storm-device/src/main/resources/appliation.yml @@ -0,0 +1,27 @@ +# knife4j配置 +#storm: +# swagger: +# enable: true +# enableResponseWrap: true +# gatewayUrl: http://localhost:8080/${spring.application.name} +# package-path: com.storm.device.controller +# title: 具身风暴 - 设备管理接口文档 +# description: 该服务包含设备有关的功能 +# contact-name: storm +# contact-url: https://storm.vip +# contact-email: zx1697935564@163.com +# version: v1.0 +# 添加SpringDoc基本配置 +#springdoc: +# api-docs: +# path: /v3/api-docs +# enabled: true +# swagger-ui: +# path: /swagger-ui.html + + + + + + + diff --git a/storm-device/src/main/resources/bootstrap.yml b/storm-device/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..8560440 --- /dev/null +++ b/storm-device/src/main/resources/bootstrap.yml @@ -0,0 +1,64 @@ +# Tomcat +server: + port: 9000 + +# Spring +spring: + datasource: + url: jdbc:mysql://127.0.0.1:3306/storm-device?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: abc123 + application: + # 应用名称 + name: storm-device + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 127.0.0.1:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: 127.0.0.1:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} +huaweicloud: + ak: OD4WEG0X1TYXQUSLM5LM + sk: QQTf5rBpvdnq1qf1OEj0VFHinFQIqW7vPN0N8vpN + + regionId: cn-south-1 + endpoint: 6396c4c5d6.st1.iotda-app.cn-south-1.myhuaweicloud.com + projectId: 673602f0d14760402fbd033b + + host: 6396c4c5d6.st1.iotda-app.cn-south-1.myhuaweicloud.com + accessKey: 3p7IyrJj + accessCode: GM6F9CbdxgLFrg22EVhDyqYh9PQ6lNqZ + queueName: DefaultQueue + +# MyBatisPlus配置 +mybatis-plus: + # 搜索指定包别名 + typeAliasesPackage: com.storm.**.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 全局配置 + global-config: + db-config: + id-type: auto #id生成策略为自增 + # 字段不存在时自动忽略填充 + ignore-invalid-fields: true + configuration: + map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名 diff --git a/storm-device/src/main/resources/bootstrap.yml01 b/storm-device/src/main/resources/bootstrap.yml01 new file mode 100644 index 0000000..b88dbe0 --- /dev/null +++ b/storm-device/src/main/resources/bootstrap.yml01 @@ -0,0 +1,59 @@ +# Tomcat +server: + port: 9000 + +# Spring +spring: + application: + # 应用名称 + name: storm-device + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} +huaweicloud: + ak: OD4WEG0X1TYXQUSLM5LM + sk: QQTf5rBpvdnq1qf1OEj0VFHinFQIqW7vPN0N8vpN + + regionId: cn-south-1 + endpoint: 6396c4c5d6.st1.iotda-app.cn-south-1.myhuaweicloud.com + projectId: 673602f0d14760402fbd033b + + host: 6396c4c5d6.st1.iotda-app.cn-south-1.myhuaweicloud.com + accessKey: 3p7IyrJj + accessCode: GM6F9CbdxgLFrg22EVhDyqYh9PQ6lNqZ + queueName: DefaultQueue + +# MyBatisPlus配置 +mybatis-plus: + # 搜索指定包别名 + typeAliasesPackage: com.storm.**.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 全局配置 + global-config: + db-config: + id-type: auto #id生成策略为自增 + # 字段不存在时自动忽略填充 + ignore-invalid-fields: true + configuration: + map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名 diff --git a/storm-device/src/main/resources/logback.xml b/storm-device/src/main/resources/logback.xml new file mode 100644 index 0000000..a534694 --- /dev/null +++ b/storm-device/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/storm-device/src/main/resources/mapper/DeviceHeadUsageMapper.xml b/storm-device/src/main/resources/mapper/DeviceHeadUsageMapper.xml new file mode 100644 index 0000000..4f57ea2 --- /dev/null +++ b/storm-device/src/main/resources/mapper/DeviceHeadUsageMapper.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + select id, device_id, head_type, usage_count, total_duration, daily_duration, weekly_duration, monthly_duration, last_used_time, update_time from device_head_usage + + + + + + + + insert into device_head_usage + + device_id, + head_type, + usage_count, + total_duration, + daily_duration, + weekly_duration, + monthly_duration, + last_used_time, + update_time, + + + #{deviceId}, + #{headType}, + #{usageCount}, + #{totalDuration}, + #{dailyDuration}, + #{weeklyDuration}, + #{monthlyDuration}, + #{lastUsedTime}, + #{updateTime}, + + + + + update device_head_usage + + device_id = #{deviceId}, + head_type = #{headType}, + usage_count = #{usageCount}, + total_duration = #{totalDuration}, + daily_duration = #{dailyDuration}, + weekly_duration = #{weeklyDuration}, + monthly_duration = #{monthlyDuration}, + last_used_time = #{lastUsedTime}, + update_time = #{updateTime}, + + where id = #{id} + + + + delete from device_head_usage where id = #{id} + + + + delete from device_head_usage where id in + + #{id} + + + \ No newline at end of file diff --git a/storm-device/src/main/resources/mapper/DeviceMapper.xml b/storm-device/src/main/resources/mapper/DeviceMapper.xml new file mode 100644 index 0000000..3f8faf0 --- /dev/null +++ b/storm-device/src/main/resources/mapper/DeviceMapper.xml @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, device_id, device_name, product_id, product_name, device_type, status, location, shop_id, longitude, latitude, software_version, vtxdb_version, last_online_time, last_offline_time, activation_time, total_online_duration, daily_duration, weekly_duration, monthly_duration, description, remark, is_deleted, create_by, update_by, create_time, update_time from device + + + + + INSERT IGNORE INTO device + + device_id, + device_name, + product_id, + product_name, + device_type, + status, + location, + shop_id, + longitude, + latitude, + software_version, + vtxdb_version, + last_online_time, + last_offline_time, + activation_time, + total_online_duration, + daily_duration, + weekly_duration, + monthly_duration, + description, + remark, + is_deleted, + create_by, + update_by, + create_time, + update_time, + + + #{deviceId}, + #{deviceName}, + #{productId}, + #{productName}, + #{deviceType}, + #{status}, + #{location}, + #{shopId}, + #{longitude}, + #{latitude}, + #{softwareVersion}, + #{vtxdbVersion}, + #{lastOnlineTime}, + #{lastOfflineTime}, + #{activationTime}, + #{totalOnlineDuration}, + #{dailyDuration}, + #{weeklyDuration}, + #{monthlyDuration}, + #{description}, + #{remark}, + #{isDeleted}, + #{createBy}, + #{updateBy}, + #{createTime}, + #{updateTime}, + + + + + + + + + insert into device + + device_id, + device_name, + product_id, + product_name, + device_type, + status, + location, + shop_id, + longitude, + latitude, + software_version, + vtxdb_version, + last_online_time, + last_offline_time, + activation_time, + total_online_duration, + daily_duration, + weekly_duration, + monthly_duration, + description, + remark, + is_deleted, + create_by, + update_by, + create_time, + update_time, + + + #{deviceId}, + #{deviceName}, + #{productId}, + #{productName}, + #{deviceType}, + #{status}, + #{location}, + #{shopId}, + #{longitude}, + #{latitude}, + #{softwareVersion}, + #{vtxdbVersion}, + #{lastOnlineTime}, + #{lastOfflineTime}, + #{activationTime}, + #{totalOnlineDuration}, + #{dailyDuration}, + #{weeklyDuration}, + #{monthlyDuration}, + #{description}, + #{remark}, + #{isDeleted}, + #{createBy}, + #{updateBy}, + #{createTime}, + #{updateTime}, + + + + + update device + + device_id = #{deviceId}, + device_name = #{deviceName}, + product_id = #{productId}, + product_name = #{productName}, + device_type = #{deviceType}, + status = #{status}, + location = #{location}, + shop_id = #{shopId}, + longitude = #{longitude}, + latitude = #{latitude}, + software_version = #{softwareVersion}, + vtxdb_version = #{vtxdbVersion}, + last_online_time = #{lastOnlineTime}, + last_offline_time = #{lastOfflineTime}, + activation_time = #{activationTime}, + total_online_duration = #{totalOnlineDuration}, + daily_duration = #{dailyDuration}, + weekly_duration = #{weeklyDuration}, + monthly_duration = #{monthlyDuration}, + description = #{description}, + remark = #{remark}, + is_deleted = #{isDeleted}, + create_by = #{createBy}, + update_by = #{updateBy}, + create_time = #{createTime}, + update_time = #{updateTime}, + + where id = #{id} + + + + delete from device where id = #{id} + + + + delete from device where id in + + #{id} + + + \ No newline at end of file diff --git a/storm-device/src/main/resources/mapper/DeviceRuntimeStatsMapper.xml b/storm-device/src/main/resources/mapper/DeviceRuntimeStatsMapper.xml new file mode 100644 index 0000000..636b79a --- /dev/null +++ b/storm-device/src/main/resources/mapper/DeviceRuntimeStatsMapper.xml @@ -0,0 +1,155 @@ + + + + + + + + + + insert into device_runtime_stats + + device_id, + stat_date, + daily_duration, + weekly_duration, + monthly_duration, + online_count, + create_time, + update_time, + + + #{deviceId}, + #{statDate}, + #{dailyDuration}, + #{weeklyDuration}, + #{monthlyDuration}, + #{onlineCount}, + #{createTime}, + #{updateTime}, + + + + + update device_runtime_stats + + device_id = #{deviceId}, + stat_date = #{statDate}, + daily_duration = #{dailyDuration}, + weekly_duration = #{weeklyDuration}, + monthly_duration = #{monthlyDuration}, + online_count = #{onlineCount}, + create_time = #{createTime}, + update_time = #{updateTime}, + + where id = #{id} + + + + delete from device_runtime_stats where id = #{id} + + + + delete from device_runtime_stats where id in + + #{id} + + + + + INSERT INTO device_runtime_stats + (device_id, stat_date, daily_duration, weekly_duration, monthly_duration, online_count, create_time, update_time, create_by) + VALUES + (#{deviceId}, #{statDate}, #{dailyDuration}, #{weeklyDuration}, #{monthlyDuration}, #{onlineCount}, #{createTime}, #{updateTime}, #{createBy}) + ON DUPLICATE KEY UPDATE + daily_duration = daily_duration + VALUES(daily_duration), + weekly_duration = weekly_duration + VALUES(weekly_duration), + monthly_duration = monthly_duration + VALUES(monthly_duration), + online_count = online_count + VALUES(online_count), + update_time = VALUES(update_time) + + + + + + + + + + + + + + + + + + select id, device_id, stat_date, daily_duration, weekly_duration, monthly_duration, online_count, create_time, update_time from device_runtime_stats + + + + + + + + + + + + + + UPDATE device_runtime_stats + SET weekly_duration = weekly_duration + #{duration} + WHERE device_id = #{deviceId} + AND stat_date >= #{weekStartDate} + AND stat_date <= #{statDate} + + + + + UPDATE device_runtime_stats + SET monthly_duration = monthly_duration + #{duration} + WHERE device_id = #{deviceId} + AND stat_date >= #{monthStartDate} + AND stat_date <= #{statDate} + + + + + \ No newline at end of file diff --git a/storm-device/src/main/resources/mapper/DeviceStatusLogMapper.xml b/storm-device/src/main/resources/mapper/DeviceStatusLogMapper.xml new file mode 100644 index 0000000..fc69ce5 --- /dev/null +++ b/storm-device/src/main/resources/mapper/DeviceStatusLogMapper.xml @@ -0,0 +1,169 @@ + + + + + + + + + insert into device_status_log + + device_id, + status, + event_time, + last_online_time, + duration, + create_time, + + + #{deviceId}, + #{status}, + #{eventTime}, + #{lastOnlineTime}, + #{duration}, + #{createTime}, + + + + + update device_status_log + + device_id = #{deviceId}, + status = #{status}, + event_time = #{eventTime}, + last_online_time = #{lastOnlineTime}, + duration = #{duration}, + create_time = #{createTime}, + + where id = #{id} + + + + delete from device_status_log where id = #{id} + + + + delete from device_status_log where id in + + #{id} + + + + + + + + + + + + + + + + + + + + + + select id, device_id, status, event_type, massage_start_time, massage_end_time, massage_duration, head_type, body_part, massage_plan, event_time, last_online_time, duration, create_time from device_status_log + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/storm-device/src/main/resources/mapper/MassageTaskMapper.xml b/storm-device/src/main/resources/mapper/MassageTaskMapper.xml new file mode 100644 index 0000000..cd4c56d --- /dev/null +++ b/storm-device/src/main/resources/mapper/MassageTaskMapper.xml @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + select id, device_id, device_name, product_name, head_type, body_part, task_time, start_time, end_time, + create_time from massage_task + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into massage_task + + device_id, + device_name, + product_name, + head_type, + body_part, + task_time, + start_time, + end_time, + create_time, + + + #{deviceId}, + #{deviceName}, + #{productName}, + #{headType}, + #{bodyPart}, + #{taskTime}, + #{startTime}, + #{endTime}, + #{createTime}, + + + + + update massage_task + + device_id = #{deviceId}, + device_name = #{deviceName}, + product_name = #{productName}, + head_type = #{headType}, + body_part = #{bodyPart}, + task_time = #{taskTime}, + start_time = #{startTime}, + end_time = #{endTime}, + create_time = #{createTime}, + + where id = #{id} + + + + delete from massage_task where id = #{id} + + + + delete from massage_task where id in + + #{id} + + + \ No newline at end of file diff --git a/storm-device/src/test/java/com/storm/device/1.json b/storm-device/src/test/java/com/storm/device/1.json new file mode 100644 index 0000000..f70ad2c --- /dev/null +++ b/storm-device/src/test/java/com/storm/device/1.json @@ -0,0 +1,24 @@ +[ + JmsSession + [ + ID + : + 51c3b405-6453-46e2-b41a-8aab14796183 + : + 1 + : + 1 + ] + delivery + dispatcher +] INFO com.iot.amqp.examples.AbstractAmqpExample - receive a message,content={"resource": "device.property", "event": "report","event_time": "20250830T062115Z", "event_time_ms": "2025-08-30T06:21:15.731Z", "request_id":"64840a97-683c-495d-af14-19f6f9e18b20", "notify_data": {"header": {"app_id":"9782371cee894c0abbecb30efe4e7cd0", "device_id": "673602f0d14760402fbd033b_jsfb-DA", "node_id": "jsfb-DA","product_id": "673602f0d14760402fbd033b", "gateway_id": "673602f0d14760402fbd033b_jsfb-DA"}, "body": {"services":[{"service_id": "VortXDB", "properties": {"online_time":"20250830T062115Z", "latitude": "112.541115", "software_version": "ioT","vtxdb_version": "1.1.9-beta-ansun", "status": "ONLINE", "longitude":"28.280514"}, "event_time": "20250830T062115Z"}] +} +} +} +{"resource": "device.property", "event": "report", "event_time": "20250830T062115Z", "event_time_ms": "2025-08-30T06:21:15.731Z", "request_id": "64840a97-683c-495d-af14-19f6f9e18b20", +"notify_data": {"header": +{"app_id": "9782371cee894c0abbecb30efe4e7cd0", "device_id": "673602f0d14760402fbd033b_jsfb-DA", "node_id": "jsfb-DA", "product_id": "673602f0d14760402fbd033b", "gateway_id": "673602f0d14760402fbd033b_jsfb-DA"}, +"body": {"services"} +:[{"service_id": "VortXDB", +"properties": {"online_time":"20250830T062115Z", "latitude": "112.541115", "software_version": "ioT","vtxdb_version": "1.1.9-beta-ansun", "status": "ONLINE", "longitude":"28.280514"}, "event_time": "20250830T062115Z" +} \ No newline at end of file diff --git a/storm-device/src/test/java/com/storm/device/2.json b/storm-device/src/test/java/com/storm/device/2.json new file mode 100644 index 0000000..ee8e8b4 --- /dev/null +++ b/storm-device/src/test/java/com/storm/device/2.json @@ -0,0 +1,659 @@ +{ + "devices": [ + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-DA", + "nodeId": "jsfb-DA", + "gatewayId": "673602f0d14760402fbd033b_jsfb-DA", + "deviceName": "jsfb-DA", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-023486", + "nodeId": "jsfb-023486", + "gatewayId": "673602f0d14760402fbd033b_jsfb-023486", + "deviceName": "jsfb-023486", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-6FF12E", + "nodeId": "jsfb-6FF12E", + "gatewayId": "673602f0d14760402fbd033b_jsfb-6FF12E", + "deviceName": "jsfb-6FF12E", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-62B336", + "nodeId": "jsfb-62B336", + "gatewayId": "673602f0d14760402fbd033b_jsfb-62B336", + "deviceName": "jsfb-62B336", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-F706AD", + "nodeId": "jsfb-F706AD", + "gatewayId": "673602f0d14760402fbd033b_jsfb-F706AD", + "deviceName": "jsfb-F706AD", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-50441C", + "nodeId": "jsfb-50441C", + "gatewayId": "673602f0d14760402fbd033b_jsfb-50441C", + "deviceName": "jsfb-50441C", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-B25A00", + "nodeId": "jsfb-B25A00", + "gatewayId": "673602f0d14760402fbd033b_jsfb-B25A00", + "deviceName": "jsfb-B25A00", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-DCF894", + "nodeId": "jsfb-DCF894", + "gatewayId": "673602f0d14760402fbd033b_jsfb-DCF894", + "deviceName": "jsfb-DCF894", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-6F0F92", + "nodeId": "jsfb-6F0F92", + "gatewayId": "673602f0d14760402fbd033b_jsfb-6F0F92", + "deviceName": "jsfb-6F0F92", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-5A1B1E", + "nodeId": "jsfb-5A1B1E", + "gatewayId": "673602f0d14760402fbd033b_jsfb-5A1B1E", + "deviceName": "jsfb-5A1B1E", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-1DB8E2", + "nodeId": "jsfb-1DB8E2", + "gatewayId": "673602f0d14760402fbd033b_jsfb-1DB8E2", + "deviceName": "jsfb-1DB8E2", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-FD7C6B", + "nodeId": "jsfb-FD7C6B", + "gatewayId": "673602f0d14760402fbd033b_jsfb-FD7C6B", + "deviceName": "jsfb-FD7C6B", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-3554A0", + "nodeId": "jsfb-3554A0", + "gatewayId": "673602f0d14760402fbd033b_jsfb-3554A0", + "deviceName": "jsfb-3554A0", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-BD51A6", + "nodeId": "jsfb-BD51A6", + "gatewayId": "673602f0d14760402fbd033b_jsfb-BD51A6", + "deviceName": "jsfb-BD51A6", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-98BF09", + "nodeId": "jsfb-98BF09", + "gatewayId": "673602f0d14760402fbd033b_jsfb-98BF09", + "deviceName": "jsfb-98BF09", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-B6A9C3", + "nodeId": "jsfb-B6A9C3", + "gatewayId": "673602f0d14760402fbd033b_jsfb-B6A9C3", + "deviceName": "jsfb-B6A9C3", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-EBF1D8", + "nodeId": "jsfb-EBF1D8", + "gatewayId": "673602f0d14760402fbd033b_jsfb-EBF1D8", + "deviceName": "jsfb-EBF1D8", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-65024A", + "nodeId": "jsfb-65024A", + "gatewayId": "673602f0d14760402fbd033b_jsfb-65024A", + "deviceName": "jsfb-65024A", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-7FC7DD", + "nodeId": "jsfb-7FC7DD", + "gatewayId": "673602f0d14760402fbd033b_jsfb-7FC7DD", + "deviceName": "jsfb-7FC7DD", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-7E9B30", + "nodeId": "jsfb-7E9B30", + "gatewayId": "673602f0d14760402fbd033b_jsfb-7E9B30", + "deviceName": "jsfb-7E9B30", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-56A928", + "nodeId": "jsfb-56A928", + "gatewayId": "673602f0d14760402fbd033b_jsfb-56A928", + "deviceName": "jsfb-56A928", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-F8843B", + "nodeId": "jsfb-F8843B", + "gatewayId": "673602f0d14760402fbd033b_jsfb-F8843B", + "deviceName": "jsfb-F8843B", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-8AA2BD", + "nodeId": "jsfb-8AA2BD", + "gatewayId": "673602f0d14760402fbd033b_jsfb-8AA2BD", + "deviceName": "jsfb-8AA2BD", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-C17E68", + "nodeId": "jsfb-C17E68", + "gatewayId": "673602f0d14760402fbd033b_jsfb-C17E68", + "deviceName": "jsfb-C17E68", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-7BE292", + "nodeId": "jsfb-7BE292", + "gatewayId": "673602f0d14760402fbd033b_jsfb-7BE292", + "deviceName": "jsfb-7BE292", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-6A1F29", + "nodeId": "jsfb-6A1F29", + "gatewayId": "673602f0d14760402fbd033b_jsfb-6A1F29", + "deviceName": "jsfb-6A1F29", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-B75756", + "nodeId": "jsfb-B75756", + "gatewayId": "673602f0d14760402fbd033b_jsfb-B75756", + "deviceName": "jsfb-B75756", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-69B0D2", + "nodeId": "jsfb-69B0D2", + "gatewayId": "673602f0d14760402fbd033b_jsfb-69B0D2", + "deviceName": "jsfb-69B0D2", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-D120B0", + "nodeId": "jsfb-D120B0", + "gatewayId": "673602f0d14760402fbd033b_jsfb-D120B0", + "deviceName": "jsfb-D120B0", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-CA0D7A", + "nodeId": "jsfb-CA0D7A", + "gatewayId": "673602f0d14760402fbd033b_jsfb-CA0D7A", + "deviceName": "jsfb-CA0D7A", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-89122A", + "nodeId": "jsfb-89122A", + "gatewayId": "673602f0d14760402fbd033b_jsfb-89122A", + "deviceName": "jsfb-89122A", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-31464E", + "nodeId": "jsfb-31464E", + "gatewayId": "673602f0d14760402fbd033b_jsfb-31464E", + "deviceName": "jsfb-31464E", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-2A1577", + "nodeId": "jsfb-2A1577", + "gatewayId": "673602f0d14760402fbd033b_jsfb-2A1577", + "deviceName": "jsfb-2A1577", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-1EC625", + "nodeId": "jsfb-1EC625", + "gatewayId": "673602f0d14760402fbd033b_jsfb-1EC625", + "deviceName": "jsfb-1EC625", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-36350C", + "nodeId": "jsfb-36350C", + "gatewayId": "673602f0d14760402fbd033b_jsfb-36350C", + "deviceName": "jsfb-36350C", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-F29258", + "nodeId": "jsfb-F29258", + "gatewayId": "673602f0d14760402fbd033b_jsfb-F29258", + "deviceName": "jsfb-F29258", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-8942CE", + "nodeId": "jsfb-8942CE", + "gatewayId": "673602f0d14760402fbd033b_jsfb-8942CE", + "deviceName": "jsfb-8942CE", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-B1398A", + "nodeId": "jsfb-B1398A", + "gatewayId": "673602f0d14760402fbd033b_jsfb-B1398A", + "deviceName": "jsfb-B1398A", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-465F0C", + "nodeId": "jsfb-465F0C", + "gatewayId": "673602f0d14760402fbd033b_jsfb-465F0C", + "deviceName": "jsfb-465F0C", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-E169C2", + "nodeId": "jsfb-E169C2", + "gatewayId": "673602f0d14760402fbd033b_jsfb-E169C2", + "deviceName": "jsfb-E169C2", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-E82E16", + "nodeId": "jsfb-E82E16", + "gatewayId": "673602f0d14760402fbd033b_jsfb-E82E16", + "deviceName": "jsfb-E82E16", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-6FA0C5", + "nodeId": "jsfb-6FA0C5", + "gatewayId": "673602f0d14760402fbd033b_jsfb-6FA0C5", + "deviceName": "jsfb-6FA0C5", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-FC04F2", + "nodeId": "jsfb-FC04F2", + "gatewayId": "673602f0d14760402fbd033b_jsfb-FC04F2", + "deviceName": "jsfb-FC04F2", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-C5D5F4", + "nodeId": "jsfb-C5D5F4", + "gatewayId": "673602f0d14760402fbd033b_jsfb-C5D5F4", + "deviceName": "jsfb-C5D5F4", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-B96E18", + "nodeId": "jsfb-B96E18", + "gatewayId": "673602f0d14760402fbd033b_jsfb-B96E18", + "deviceName": "jsfb-B96E18", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-37285C", + "nodeId": "jsfb-37285C", + "gatewayId": "673602f0d14760402fbd033b_jsfb-37285C", + "deviceName": "jsfb-37285C", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-00A874", + "nodeId": "jsfb-00A874", + "gatewayId": "673602f0d14760402fbd033b_jsfb-00A874", + "deviceName": "jsfb-00A874", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-7D2A0B", + "nodeId": "jsfb-7D2A0B", + "gatewayId": "673602f0d14760402fbd033b_jsfb-7D2A0B", + "deviceName": "jsfb-7D2A0B", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-69681C", + "nodeId": "jsfb-69681C", + "gatewayId": "673602f0d14760402fbd033b_jsfb-69681C", + "deviceName": "jsfb-69681C", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + }, + { + "appId": "9782371cee894c0abbecb30efe4e7cd0", + "appName": "DefaultApp_6736wwjm", + "deviceId": "673602f0d14760402fbd033b_jsfb-6D10F3", + "nodeId": "jsfb-6D10F3", + "gatewayId": "673602f0d14760402fbd033b_jsfb-6D10F3", + "deviceName": "jsfb-6D10F3", + "nodeType": "GATEWAY", + "productId": "673602f0d14760402fbd033b", + "productName": "RS-LL-X1", + "status": "INACTIVE", + "tags": [] + } + ], + "page": { + "count": 227, + "marker": "6881dce05ebba32371ca42f6" + }, + "httpStatusCode": 200 +} diff --git a/storm-device/src/test/java/com/storm/device/3.json b/storm-device/src/test/java/com/storm/device/3.json new file mode 100644 index 0000000..9661f54 --- /dev/null +++ b/storm-device/src/test/java/com/storm/device/3.json @@ -0,0 +1,62 @@ +{ + online_time=20250829T113156Z, + latitude=112.541115, + software_version=ioT, + vtxdb_version=1.1.9-beta-ansun, + status=ONLINE, + longitude=28.280514 +} + +[ + class + DeviceShadowData + { + serviceId: ExpectedDelivery + desired: class + DeviceShadowProperties + { + properties: null + eventTime: 20250830T014623Z + } + reported + : + class + DeviceShadowProperties + { + properties: null + eventTime: null + } + version + : + 750 + }, + class + DeviceShadowData + { + serviceId: VortXDB + desired: class + DeviceShadowProperties + { + properties: null + eventTime: null + } + reported + : + class + DeviceShadowProperties + { + properties: { + online_time=20250829T113156Z, + latitude=112.541115, + software_version=ioT, + vtxdb_version=1.1.9-beta-ansun, + status=ONLINE, + longitude=28.280514 + } + eventTime: 20250829T113156Z + } + version + : + 37 + } +] \ No newline at end of file diff --git a/storm-device/src/test/java/com/storm/device/4.json b/storm-device/src/test/java/com/storm/device/4.json new file mode 100644 index 0000000..f19b2f8 --- /dev/null +++ b/storm-device/src/test/java/com/storm/device/4.json @@ -0,0 +1,12 @@ +{ + "pageNo": 1, + "pageSize": 10, + "isAsc": true, + "sortBy": "id", + "deviceNameParam": "", + "statusParam": "", + "softwareVersionParam": "", + "vtxdbVersionParam": "", + "beginTime": "", + "endTime": "" +} \ No newline at end of file diff --git a/storm-device/src/test/java/com/storm/device/5.json b/storm-device/src/test/java/com/storm/device/5.json new file mode 100644 index 0000000..07bd6db --- /dev/null +++ b/storm-device/src/test/java/com/storm/device/5.json @@ -0,0 +1,3 @@ +IotMsgNotifyData(header=IotMsgHeader(productId=673602f0d14760402fbd033b, deviceId=673602f0d14760402fbd033b_jsfb-DA, nodeId=jsfb-DA, appId=9782371cee894c0abbecb30efe4e7cd0, gatewayId=673602f0d14760402fbd033b_jsfb-DA), body=IotMsgBody(services=[IotMsgService(serviceId=VortXDB, properties={online_time=20250901T032819Z, latitude=112.541115, software_version=ioT, vtxdb_version=1.1.9-beta-ansun, status=ONLINE, longitude=28.280514}, eventTime=20250901T032819Z)])) + +IotMsgNotifyData(header=IotMsgHeader(productId=673602f0d14760402fbd033b, deviceId=673602f0d14760402fbd033b_jsfb-DA, nodeId=jsfb-DA, appId=9782371cee894c0abbecb30efe4e7cd0, gatewayId=673602f0d14760402fbd033b_jsfb-DA), body=IotMsgBody(services=[IotMsgService(serviceId=VortXDB, properties={online_time=20250901T041315Z, latitude=112.541115, software_version=ioT, vtxdb_version=1.1.9-beta-ansun, status=ONLINE, longitude=28.280514}, eventTime=20250901T041316Z)])) diff --git a/storm-device/src/test/java/com/storm/device/IotTest.java b/storm-device/src/test/java/com/storm/device/IotTest.java new file mode 100644 index 0000000..642650d --- /dev/null +++ b/storm-device/src/test/java/com/storm/device/IotTest.java @@ -0,0 +1,181 @@ +package com.storm.device; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.huaweicloud.sdk.iotda.v5.IoTDAClient; +import com.huaweicloud.sdk.iotda.v5.model.*; +import com.storm.common.core.exception.base.BaseException; +import com.storm.common.core.utils.StringUtils; +import com.storm.device.domain.po.Device; +import com.storm.device.service.IDeviceService; +import com.storm.device.service.impl.DeviceServiceImpl; +import com.storm.device.service.impl.MassageTaskServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author DengAo + * @date 2025/8/27 17:11 + */ +@Slf4j +@SpringBootTest +public class IotTest { + @Resource + private IoTDAClient client; + + @Resource + private DeviceServiceImpl deviceService; + + @Test + public void testSyncDevicesFromIoT() { + List deviceList = new ArrayList<>(); + String marker = null; + boolean hasMore = true; + + try { + while (hasMore) { + // 创建请求对象 + ListDevicesRequest request = new ListDevicesRequest(); + + // 设置分页参数 + if (marker != null) { + request.setMarker(marker); + } + request.setLimit(50); // 每页最大100条 + + // 发起请求获取设备列表 + ListDevicesResponse response = client.listDevices(request); + + if (response.getHttpStatusCode() != 200) { + throw new BaseException("物联网接口 - 查询设备列表失败,HTTP状态码: " + response.getHttpStatusCode()); + } + + log.info("50台设备信息:{}", JSONUtil.toJsonStr(response.getDevices())); + + // 处理返回的设备数据 + if (CollectionUtils.isNotEmpty(response.getDevices())) { + for (QueryDeviceSimplify queryDeviceSimplify : response.getDevices()) { + Device device = new Device(); + device.setDeviceId(queryDeviceSimplify.getDeviceId()); + device.setDeviceName(queryDeviceSimplify.getDeviceName()); + device.setProductId(queryDeviceSimplify.getProductId()); + device.setProductName(queryDeviceSimplify.getProductName()); + + // 可以设置其他默认值 + device.setDeviceType(1); // 默认设备类型 + device.setIsDeleted(0); // 未删除 + + deviceList.add(device); + } + } + + // 检查是否还有更多数据 + if (CollectionUtils.isNotEmpty(response.getDevices()) && StringUtils.isNotEmpty(response.getPage().getMarker())) { + marker = response.getPage().getMarker(); + } else { + hasMore = false; + } + // 休眠100毫秒 + Thread.sleep(100); + } + + log.info("成功同步 {} 台设备信息", deviceList.size()); + } catch (Exception e) { + log.error("同步设备列表失败: {}", e.getMessage(), e); + throw new BaseException("同步设备列表失败: " + e.getMessage()); + } + } + + @Test + public void testSyncDevicesFromIoT2() { + String marker = "675299a3f9024677dbd36a08"; + //请求参数 + ListDevicesRequest request = new ListDevicesRequest(); + + //设置条数 + request.setLimit(50); + + request.setMarker(marker); + + //发起请求 + ListDevicesResponse response = client.listDevices(request); + if (response.getHttpStatusCode() != 200) { + throw new BaseException("物联网接口 - 查询设备,同步失败"); + } + + log.info("50台设备信息:{}", JSONUtil.toJsonStr(response.getDevices())); + + } + + @Test + public void updateDeviceStatusLog() { + //通过client 调用查询设备影子数据 + ShowDeviceShadowRequest request = new ShowDeviceShadowRequest(); + request.setDeviceId("673602f0d14760402fbd033b_jsfb-66E903"); + ShowDeviceShadowResponse response = client.showDeviceShadow(request); + + //解析数据 + List shadow = response.getShadow(); + if (CollUtil.isEmpty(shadow)) { + log.error("设备影子数据为空"); + return; + } + JSONObject jsonObject = JSONUtil.parseObj(shadow.get(1).getReported().getProperties()); + + String eventTimeStr = shadow.get(1).getReported().getEventTime(); + LocalDateTime time = otherLocalDateTime(eventTimeStr); + + //打印返回的结果 + log.info("设备影子数据:{}", jsonObject); + System.out.println("--------------------------------------"); + log.info("设备影子数据时间:{}", time); + // 打印shadow数据 + System.out.println("--------------------------------------"); + log.info("设备影子数据:{}", shadow); + System.out.println("--------------------------------------"); + LocalDateTime onlineTime = otherLocalDateTime(jsonObject.get("online_time", String.class)); + log.info("设备影子online_time时间:{}", onlineTime); + + } + + /** + * 测试百度地图API获取地址 + */ + @Test + public void testGetLocation() { + String location = deviceService.getLocationFromCoordinates(22.624244620774185, 114.03790755307914); + log.info("设备地址:{}", location); + } + + public LocalDateTime toLocalDateTime(String activeTime) { + LocalDateTime time = LocalDateTimeUtil.parse(activeTime, DatePattern.UTC_MS_PATTERN); + time = time.atZone(ZoneId.from(ZoneOffset.UTC)) + .withZoneSameInstant(ZoneId.of("Asia/Shanghai")) + .toLocalDateTime(); + return time; + } + + public LocalDateTime otherLocalDateTime(String activeTime) { + LocalDateTime time = LocalDateTimeUtil.parse(activeTime, "yyyyMMdd'T'HHmmss'Z'"); + time = time.atZone(ZoneId.from(ZoneOffset.UTC)) + .withZoneSameInstant(ZoneId.of("Asia/Shanghai")) + .toLocalDateTime(); + return time; + } + +} diff --git a/storm-gateway/pom.xml b/storm-gateway/pom.xml new file mode 100644 index 0000000..fad4794 --- /dev/null +++ b/storm-gateway/pom.xml @@ -0,0 +1,141 @@ + + + com.storm + storm + 3.6.6 + + 4.0.0 + + storm-gateway + + + storm-gateway网关模块 + + + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + javax.servlet + servlet-api + + + + + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel-gateway + + + + + com.alibaba.csp + sentinel-datasource-nacos + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.cloud + spring-cloud-loadbalancer + + + + + pro.fessional + kaptcha + + + javax.servlet + servlet-api + + + + + + + com.storm + storm-common-redis + + + + + com.github.xiaoymin + knife4j-gateway-spring-boot-starter + 4.3.0 + + + + + org.projectlombok + lombok + + + + com.storm + storm-common-core + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework + spring-webmvc + + + org.springframework + spring-web + + + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + diff --git a/storm-gateway/src/main/java/com/storm/gateway/GatewayApplication.java b/storm-gateway/src/main/java/com/storm/gateway/GatewayApplication.java new file mode 100644 index 0000000..8145229 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/GatewayApplication.java @@ -0,0 +1,43 @@ +package com.storm.gateway; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.env.Environment; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 网关启动程序 + * + * @author ruoyi + */ +@Slf4j +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class }) +public class GatewayApplication +{ + public static void main(String[] args) throws UnknownHostException { + SpringApplication app = new SpringApplicationBuilder(GatewayApplication.class).build(args); + Environment env = app.run(args).getEnvironment(); + String protocol = "http"; + if (env.getProperty("server.ssl.key-store") != null) { + protocol = "https"; + } + log.info("--/\n---------------------------------------------------------------------------------------\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\t{}://localhost:{}\n\t" + + "External: \t{}://{}:{}\n\t" + + "Profile(s): \t{}" + + "\n---------------------------------------------------------------------------------------", + env.getProperty("spring.application.name"), + protocol, + env.getProperty("server.port"), + protocol, + InetAddress.getLocalHost().getHostAddress(), + env.getProperty("server.port"), + env.getActiveProfiles()); + } +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/config/CaptchaConfig.java b/storm-gateway/src/main/java/com/storm/gateway/config/CaptchaConfig.java new file mode 100644 index 0000000..7e23425 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/config/CaptchaConfig.java @@ -0,0 +1,83 @@ +package com.storm.gateway.config; + +import java.util.Properties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + * + * @author ruoyi + */ +@Configuration +public class CaptchaConfig +{ + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + @Bean(name = "captchaProducerMath") + public DefaultKaptcha getKaptchaBeanMath() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 边框颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); + // 验证码文本生成器 + properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.storm.gateway.config.KaptchaTextCreator"); + // 验证码文本字符间距 默认为2 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 验证码噪点颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); + // 干扰实现类 + properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} \ No newline at end of file diff --git a/storm-gateway/src/main/java/com/storm/gateway/config/GatewayConfig.java b/storm-gateway/src/main/java/com/storm/gateway/config/GatewayConfig.java new file mode 100644 index 0000000..24e2a88 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/config/GatewayConfig.java @@ -0,0 +1,23 @@ +package com.storm.gateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import com.storm.gateway.handler.SentinelFallbackHandler; + +/** + * 网关限流配置 + * + * @author ruoyi + */ +@Configuration +public class GatewayConfig +{ + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SentinelFallbackHandler sentinelGatewayExceptionHandler() + { + return new SentinelFallbackHandler(); + } +} \ No newline at end of file diff --git a/storm-gateway/src/main/java/com/storm/gateway/config/KaptchaTextCreator.java b/storm-gateway/src/main/java/com/storm/gateway/config/KaptchaTextCreator.java new file mode 100644 index 0000000..b7e5402 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/config/KaptchaTextCreator.java @@ -0,0 +1,75 @@ +package com.storm.gateway.config; + +import java.util.Random; +import com.google.code.kaptcha.text.impl.DefaultTextCreator; + +/** + * 验证码文本生成器 + * + * @author ruoyi + */ +public class KaptchaTextCreator extends DefaultTextCreator +{ + private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); + + @Override + public String getText() + { + Integer result = 0; + Random random = new Random(); + int x = random.nextInt(10); + int y = random.nextInt(10); + StringBuilder suChinese = new StringBuilder(); + int randomoperands = random.nextInt(3); + if (randomoperands == 0) + { + result = x * y; + suChinese.append(CNUMBERS[x]); + suChinese.append("*"); + suChinese.append(CNUMBERS[y]); + } + else if (randomoperands == 1) + { + if ((x != 0) && y % x == 0) + { + result = y / x; + suChinese.append(CNUMBERS[y]); + suChinese.append("/"); + suChinese.append(CNUMBERS[x]); + } + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + } + else if (randomoperands == 2) + { + if (x >= y) + { + result = x - y; + suChinese.append(CNUMBERS[x]); + suChinese.append("-"); + suChinese.append(CNUMBERS[y]); + } + else + { + result = y - x; + suChinese.append(CNUMBERS[y]); + suChinese.append("-"); + suChinese.append(CNUMBERS[x]); + } + } + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + suChinese.append("=?@" + result); + return suChinese.toString(); + } +} \ No newline at end of file diff --git a/storm-gateway/src/main/java/com/storm/gateway/config/Knife4jGatewayConfig.java b/storm-gateway/src/main/java/com/storm/gateway/config/Knife4jGatewayConfig.java new file mode 100644 index 0000000..a7c93ef --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/config/Knife4jGatewayConfig.java @@ -0,0 +1,107 @@ +/* +package com.storm.gateway.config; + +import java.util.Set; +import java.util.stream.Collectors; +import org.springdoc.core.AbstractSwaggerUiConfigProperties; +import org.springdoc.core.SwaggerUiConfigProperties; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.annotation.Configuration; +import com.alibaba.nacos.client.naming.event.InstancesChangeEvent; +import com.alibaba.nacos.common.notify.Event; +import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.notify.listener.Subscriber; +import com.storm.common.core.utils.StringUtils; + +*/ +/** + * Knife4j 网关文档聚合配置类 + * + * @author storm + *//* + +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "knife4j.gateway.enabled", matchIfMissing = true) +public class Knife4jGatewayConfig implements InitializingBean +{ + @Autowired + private Knife4jGatewayProperties knife4jGatewayProperties; + + @Autowired + private DiscoveryClient discoveryClient; + + */ +/** + * 在初始化后调用的方法 + *//* + + @Override + public void afterPropertiesSet() + { + NotifyCenter.registerSubscriber(new Knife4jDocRegister(knife4jGatewayProperties, discoveryClient)); + } +} + +*/ +/** + * Swagger文档注册器 + *//* + +class Knife4jDocRegister extends Subscriber +{ + @Autowired + private Knife4jGatewayProperties knife4jGatewayProperties; + + @Autowired + private DiscoveryClient discoveryClient; + + private final static String[] EXCLUDE_ROUTES = new String[] { "storm-gateway", "storm-auth", "storm-file", "storm-monitor" }; + + public Knife4jDocRegister(Knife4jGatewayProperties knife4jGatewayProperties, DiscoveryClient discoveryClient) + { + this.knife4jGatewayProperties = knife4jGatewayProperties; + this.discoveryClient = discoveryClient; + } + + */ +/** + * 事件回调方法,处理InstancesChangeEvent事件 + * @param event 事件对象 + *//* + + @Override + public void onEvent(InstancesChangeEvent event) + { + Set routes = discoveryClient.getServices() + .stream() + .flatMap(serviceId -> discoveryClient.getInstances(serviceId).stream()) + .filter(instance -> !StringUtils.equalsAnyIgnoreCase(instance.getServiceId(), EXCLUDE_ROUTES)) + .map(instance -> { + GatewayRoute route = new GatewayRoute(); + route.setName(instance.getServiceId()); + route.setUrl(String.format("/%s/v3/api-docs", instance.getServiceId())); + route.setServiceName(instance.getServiceId()); + route.setOrder(0); + return route; + }) + .collect(Collectors.toSet()); + + knife4jGatewayProperties.setRoutes(routes); + } + + */ +/** + * 订阅类型方法,返回订阅的事件类型 + * @return 订阅的事件类型 + *//* + + @Override + public Class subscribeType() + { + return InstancesChangeEvent.class; + } +} +*/ diff --git a/storm-gateway/src/main/java/com/storm/gateway/config/RouterFunctionConfiguration.java b/storm-gateway/src/main/java/com/storm/gateway/config/RouterFunctionConfiguration.java new file mode 100644 index 0000000..6d0bb94 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/config/RouterFunctionConfiguration.java @@ -0,0 +1,31 @@ +package com.storm.gateway.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import com.storm.gateway.handler.ValidateCodeHandler; + +/** + * 路由配置信息 + * + * @author ruoyi + */ +@Configuration +public class RouterFunctionConfiguration +{ + @Autowired + private ValidateCodeHandler validateCodeHandler; + + @SuppressWarnings("rawtypes") + @Bean + public RouterFunction routerFunction() + { + return RouterFunctions.route( + RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), + validateCodeHandler); + } +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/config/properties/CaptchaProperties.java b/storm-gateway/src/main/java/com/storm/gateway/config/properties/CaptchaProperties.java new file mode 100644 index 0000000..40f9e64 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/config/properties/CaptchaProperties.java @@ -0,0 +1,46 @@ +package com.storm.gateway.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +/** + * 验证码配置 + * + * @author ruoyi + */ +@Configuration +@RefreshScope +@ConfigurationProperties(prefix = "security.captcha") +public class CaptchaProperties +{ + /** + * 验证码开关 + */ + private Boolean enabled; + + /** + * 验证码类型(math 数组计算 char 字符) + */ + private String type; + + public Boolean getEnabled() + { + return enabled; + } + + public void setEnabled(Boolean enabled) + { + this.enabled = enabled; + } + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/config/properties/IgnoreWhiteProperties.java b/storm-gateway/src/main/java/com/storm/gateway/config/properties/IgnoreWhiteProperties.java new file mode 100644 index 0000000..fb2074c --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/config/properties/IgnoreWhiteProperties.java @@ -0,0 +1,33 @@ +package com.storm.gateway.config.properties; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +/** + * 放行白名单配置 + * + * @author ruoyi + */ +@Configuration +@RefreshScope +@ConfigurationProperties(prefix = "security.ignore") +public class IgnoreWhiteProperties +{ + /** + * 放行白名单配置,网关不校验此处的白名单 + */ + private List whites = new ArrayList<>(); + + public List getWhites() + { + return whites; + } + + public void setWhites(List whites) + { + this.whites = whites; + } +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/config/properties/XssProperties.java b/storm-gateway/src/main/java/com/storm/gateway/config/properties/XssProperties.java new file mode 100644 index 0000000..5fccb2f --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/config/properties/XssProperties.java @@ -0,0 +1,48 @@ +package com.storm.gateway.config.properties; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +/** + * XSS跨站脚本配置 + * + * @author ruoyi + */ +@Configuration +@RefreshScope +@ConfigurationProperties(prefix = "security.xss") +public class XssProperties +{ + /** + * Xss开关 + */ + private Boolean enabled; + + /** + * 排除路径 + */ + private List excludeUrls = new ArrayList<>(); + + public Boolean getEnabled() + { + return enabled; + } + + public void setEnabled(Boolean enabled) + { + this.enabled = enabled; + } + + public List getExcludeUrls() + { + return excludeUrls; + } + + public void setExcludeUrls(List excludeUrls) + { + this.excludeUrls = excludeUrls; + } +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/filter/AuthFilter.java b/storm-gateway/src/main/java/com/storm/gateway/filter/AuthFilter.java new file mode 100644 index 0000000..4a2f9b6 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/filter/AuthFilter.java @@ -0,0 +1,135 @@ +package com.storm.gateway.filter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import com.storm.common.core.constant.CacheConstants; +import com.storm.common.core.constant.HttpStatus; +import com.storm.common.core.constant.SecurityConstants; +import com.storm.common.core.constant.TokenConstants; +import com.storm.common.core.utils.JwtUtils; +import com.storm.common.core.utils.ServletUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.redis.service.RedisService; +import com.storm.gateway.config.properties.IgnoreWhiteProperties; +import io.jsonwebtoken.Claims; +import reactor.core.publisher.Mono; + +/** + * 网关鉴权 + * + * @author ruoyi + */ +@Component +public class AuthFilter implements GlobalFilter, Ordered +{ + private static final Logger log = LoggerFactory.getLogger(AuthFilter.class); + + // 排除过滤的 uri 地址,nacos自行添加 + @Autowired + private IgnoreWhiteProperties ignoreWhite; + + @Autowired + private RedisService redisService; + + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) + { + ServerHttpRequest request = exchange.getRequest(); + ServerHttpRequest.Builder mutate = request.mutate(); + + String url = request.getURI().getPath(); + // 跳过不需要验证的路径 + if (StringUtils.matches(url, ignoreWhite.getWhites())) + { + return chain.filter(exchange); + } + String token = getToken(request); + if (StringUtils.isEmpty(token)) + { + return unauthorizedResponse(exchange, "令牌不能为空"); + } + Claims claims = JwtUtils.parseToken(token); + if (claims == null) + { + return unauthorizedResponse(exchange, "令牌已过期或验证不正确!"); + } + String userkey = JwtUtils.getUserKey(claims); + boolean islogin = redisService.hasKey(getTokenKey(userkey)); + if (!islogin) + { + return unauthorizedResponse(exchange, "登录状态已过期"); + } + String userid = JwtUtils.getUserId(claims); + String username = JwtUtils.getUserName(claims); + if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) + { + return unauthorizedResponse(exchange, "令牌验证失败"); + } + + // 设置用户信息到请求 + addHeader(mutate, SecurityConstants.USER_KEY, userkey); + addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid); + addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username); + // 内部请求来源参数清除 + removeHeader(mutate, SecurityConstants.FROM_SOURCE); + return chain.filter(exchange.mutate().request(mutate.build()).build()); + } + + private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) + { + if (value == null) + { + return; + } + String valueStr = value.toString(); + String valueEncode = ServletUtils.urlEncode(valueStr); + mutate.header(name, valueEncode); + } + + private void removeHeader(ServerHttpRequest.Builder mutate, String name) + { + mutate.headers(httpHeaders -> httpHeaders.remove(name)).build(); + } + + private Mono unauthorizedResponse(ServerWebExchange exchange, String msg) + { + log.error("[鉴权异常处理]请求路径:{},错误信息:{}", exchange.getRequest().getPath(), msg); + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED); + } + + /** + * 获取缓存key + */ + private String getTokenKey(String token) + { + return CacheConstants.LOGIN_TOKEN_KEY + token; + } + + /** + * 获取请求token + */ + private String getToken(ServerHttpRequest request) + { + String token = request.getHeaders().getFirst(SecurityConstants.AUTHORIZATION_HEADER); + // 如果前端设置了令牌前缀,则裁剪掉前缀 + if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) + { + token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY); + } + return token; + } + + @Override + public int getOrder() + { + return -200; + } +} \ No newline at end of file diff --git a/storm-gateway/src/main/java/com/storm/gateway/filter/BlackListUrlFilter.java b/storm-gateway/src/main/java/com/storm/gateway/filter/BlackListUrlFilter.java new file mode 100644 index 0000000..310037c --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/filter/BlackListUrlFilter.java @@ -0,0 +1,65 @@ +package com.storm.gateway.filter; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.stereotype.Component; +import com.storm.common.core.utils.ServletUtils; + +/** + * 黑名单过滤器 + * + * @author ruoyi + */ +@Component +public class BlackListUrlFilter extends AbstractGatewayFilterFactory +{ + @Override + public GatewayFilter apply(Config config) + { + return (exchange, chain) -> { + + String url = exchange.getRequest().getURI().getPath(); + if (config.matchBlacklist(url)) + { + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问"); + } + + return chain.filter(exchange); + }; + } + + public BlackListUrlFilter() + { + super(Config.class); + } + + public static class Config + { + private List blacklistUrl; + + private List blacklistUrlPattern = new ArrayList<>(); + + public boolean matchBlacklist(String url) + { + return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find()); + } + + public List getBlacklistUrl() + { + return blacklistUrl; + } + + public void setBlacklistUrl(List blacklistUrl) + { + this.blacklistUrl = blacklistUrl; + this.blacklistUrlPattern.clear(); + this.blacklistUrl.forEach(url -> { + this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE)); + }); + } + } + +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/filter/ValidateCodeFilter.java b/storm-gateway/src/main/java/com/storm/gateway/filter/ValidateCodeFilter.java new file mode 100644 index 0000000..53296ab --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/filter/ValidateCodeFilter.java @@ -0,0 +1,79 @@ +package com.storm.gateway.filter; + +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicReference; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.storm.common.core.utils.ServletUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.gateway.config.properties.CaptchaProperties; +import com.storm.gateway.service.ValidateCodeService; +import reactor.core.publisher.Flux; + +/** + * 验证码过滤器 + * + * @author ruoyi + */ +@Component +public class ValidateCodeFilter extends AbstractGatewayFilterFactory +{ + private final static String[] VALIDATE_URL = new String[] { "/auth/login", "/auth/register" }; + + @Autowired + private ValidateCodeService validateCodeService; + + @Autowired + private CaptchaProperties captchaProperties; + + private static final String CODE = "code"; + + private static final String UUID = "uuid"; + + @Override + public GatewayFilter apply(Object config) + { + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest(); + + // 非登录/注册请求或验证码关闭,不处理 + if (!StringUtils.equalsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled()) + { + return chain.filter(exchange); + } + + try + { + String rspStr = resolveBodyFromRequest(request); + JSONObject obj = JSON.parseObject(rspStr); + validateCodeService.checkCaptcha(obj.getString(CODE), obj.getString(UUID)); + } + catch (Exception e) + { + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage()); + } + return chain.filter(exchange); + }; + } + + private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) + { + // 获取请求体 + Flux body = serverHttpRequest.getBody(); + AtomicReference bodyRef = new AtomicReference<>(); + body.subscribe(buffer -> { + CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer()); + DataBufferUtils.release(buffer); + bodyRef.set(charBuffer.toString()); + }); + return bodyRef.get(); + } +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/filter/XssFilter.java b/storm-gateway/src/main/java/com/storm/gateway/filter/XssFilter.java new file mode 100644 index 0000000..dec0f33 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/filter/XssFilter.java @@ -0,0 +1,129 @@ +package com.storm.gateway.filter; + +import java.nio.charset.StandardCharsets; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.io.buffer.NettyDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.html.EscapeUtil; +import com.storm.gateway.config.properties.XssProperties; +import io.netty.buffer.ByteBufAllocator; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * 跨站脚本过滤器 + * + * @author ruoyi + */ +@Component +@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true") +public class XssFilter implements GlobalFilter, Ordered +{ + // 跨站脚本的 xss 配置,nacos自行添加 + @Autowired + private XssProperties xss; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) + { + ServerHttpRequest request = exchange.getRequest(); + // xss开关未开启 或 通过nacos关闭,不过滤 + if (!xss.getEnabled()) + { + return chain.filter(exchange); + } + // GET DELETE 不过滤 + HttpMethod method = request.getMethod(); + if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) + { + return chain.filter(exchange); + } + // 非json类型,不过滤 + if (!isJsonRequest(exchange)) + { + return chain.filter(exchange); + } + // excludeUrls 不过滤 + String url = request.getURI().getPath(); + if (StringUtils.matches(url, xss.getExcludeUrls())) + { + return chain.filter(exchange); + } + ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange); + return chain.filter(exchange.mutate().request(httpRequestDecorator).build()); + + } + + private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange) + { + ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) + { + @Override + public Flux getBody() + { + Flux body = super.getBody(); + return body.buffer().map(dataBuffers -> { + DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); + DataBuffer join = dataBufferFactory.join(dataBuffers); + byte[] content = new byte[join.readableByteCount()]; + join.read(content); + DataBufferUtils.release(join); + String bodyStr = new String(content, StandardCharsets.UTF_8); + // 防xss攻击过滤 + bodyStr = EscapeUtil.clean(bodyStr); + // 转成字节 + byte[] bytes = bodyStr.getBytes(StandardCharsets.UTF_8); + NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); + DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + return buffer; + }); + } + + @Override + public HttpHeaders getHeaders() + { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.putAll(super.getHeaders()); + // 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length + httpHeaders.remove(HttpHeaders.CONTENT_LENGTH); + httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); + return httpHeaders; + } + + }; + return serverHttpRequestDecorator; + } + + /** + * 是否是Json请求 + * + * @param exchange HTTP请求 + */ + public boolean isJsonRequest(ServerWebExchange exchange) + { + String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); + return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); + } + + @Override + public int getOrder() + { + return -100; + } +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/handler/GatewayExceptionHandler.java b/storm-gateway/src/main/java/com/storm/gateway/handler/GatewayExceptionHandler.java new file mode 100644 index 0000000..ca40248 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/handler/GatewayExceptionHandler.java @@ -0,0 +1,56 @@ +package com.storm.gateway.handler; + +import org.springframework.cloud.gateway.support.NotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import com.storm.common.core.utils.ServletUtils; +import reactor.core.publisher.Mono; + +/** + * 网关统一异常处理 + * + * @author ruoyi + */ +@Order(-1) +@Configuration +public class GatewayExceptionHandler implements ErrorWebExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class); + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) + { + ServerHttpResponse response = exchange.getResponse(); + + if (exchange.getResponse().isCommitted()) + { + return Mono.error(ex); + } + + String msg; + + if (ex instanceof NotFoundException) + { + msg = "服务未找到"; + } + else if (ex instanceof ResponseStatusException) + { + ResponseStatusException responseStatusException = (ResponseStatusException) ex; + msg = responseStatusException.getMessage(); + } + else + { + msg = "内部服务器错误"; + } + + log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex.getMessage()); + + return ServletUtils.webFluxResponseWriter(response, msg); + } +} \ No newline at end of file diff --git a/storm-gateway/src/main/java/com/storm/gateway/handler/SentinelFallbackHandler.java b/storm-gateway/src/main/java/com/storm/gateway/handler/SentinelFallbackHandler.java new file mode 100644 index 0000000..562e374 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/handler/SentinelFallbackHandler.java @@ -0,0 +1,41 @@ +package com.storm.gateway.handler; + +import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.storm.common.core.utils.ServletUtils; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebExceptionHandler; +import reactor.core.publisher.Mono; + +/** + * 自定义限流异常处理 + * + * @author ruoyi + */ +public class SentinelFallbackHandler implements WebExceptionHandler +{ + private Mono writeResponse(ServerResponse response, ServerWebExchange exchange) + { + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求超过最大数,请稍候再试"); + } + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) + { + if (exchange.getResponse().isCommitted()) + { + return Mono.error(ex); + } + if (!BlockException.isBlockException(ex)) + { + return Mono.error(ex); + } + return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange)); + } + + private Mono handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) + { + return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable); + } +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/handler/ValidateCodeHandler.java b/storm-gateway/src/main/java/com/storm/gateway/handler/ValidateCodeHandler.java new file mode 100644 index 0000000..bbbfae7 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/handler/ValidateCodeHandler.java @@ -0,0 +1,41 @@ +package com.storm.gateway.handler; + +import java.io.IOException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import com.storm.common.core.exception.CaptchaException; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.gateway.service.ValidateCodeService; +import reactor.core.publisher.Mono; + +/** + * 验证码获取 + * + * @author ruoyi + */ +@Component +public class ValidateCodeHandler implements HandlerFunction +{ + @Autowired + private ValidateCodeService validateCodeService; + + @Override + public Mono handle(ServerRequest serverRequest) + { + AjaxResult ajax; + try + { + ajax = validateCodeService.createCaptcha(); + } + catch (CaptchaException | IOException e) + { + return Mono.error(e); + } + return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(ajax)); + } +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/service/ValidateCodeService.java b/storm-gateway/src/main/java/com/storm/gateway/service/ValidateCodeService.java new file mode 100644 index 0000000..4423a94 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/service/ValidateCodeService.java @@ -0,0 +1,23 @@ +package com.storm.gateway.service; + +import java.io.IOException; +import com.storm.common.core.exception.CaptchaException; +import com.storm.common.core.web.domain.AjaxResult; + +/** + * 验证码处理 + * + * @author ruoyi + */ +public interface ValidateCodeService +{ + /** + * 生成验证码 + */ + public AjaxResult createCaptcha() throws IOException, CaptchaException; + + /** + * 校验验证码 + */ + public void checkCaptcha(String key, String value) throws CaptchaException; +} diff --git a/storm-gateway/src/main/java/com/storm/gateway/service/impl/ValidateCodeServiceImpl.java b/storm-gateway/src/main/java/com/storm/gateway/service/impl/ValidateCodeServiceImpl.java new file mode 100644 index 0000000..4964942 --- /dev/null +++ b/storm-gateway/src/main/java/com/storm/gateway/service/impl/ValidateCodeServiceImpl.java @@ -0,0 +1,118 @@ +package com.storm.gateway.service.impl; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import javax.annotation.Resource; +import javax.imageio.ImageIO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.FastByteArrayOutputStream; +import com.google.code.kaptcha.Producer; +import com.storm.common.core.constant.CacheConstants; +import com.storm.common.core.constant.Constants; +import com.storm.common.core.exception.CaptchaException; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.sign.Base64; +import com.storm.common.core.utils.uuid.IdUtils; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.redis.service.RedisService; +import com.storm.gateway.config.properties.CaptchaProperties; +import com.storm.gateway.service.ValidateCodeService; + +/** + * 验证码实现处理 + * + * @author ruoyi + */ +@Service +public class ValidateCodeServiceImpl implements ValidateCodeService +{ + @Resource(name = "captchaProducer") + private Producer captchaProducer; + + @Resource(name = "captchaProducerMath") + private Producer captchaProducerMath; + + @Autowired + private RedisService redisService; + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 生成验证码 + */ + @Override + public AjaxResult createCaptcha() throws IOException, CaptchaException + { + AjaxResult ajax = AjaxResult.success(); + boolean captchaEnabled = captchaProperties.getEnabled(); + ajax.put("captchaEnabled", captchaEnabled); + if (!captchaEnabled) + { + return ajax; + } + + // 保存验证码信息 + String uuid = IdUtils.simpleUUID(); + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; + + String capStr = null, code = null; + BufferedImage image = null; + + String captchaType = captchaProperties.getType(); + // 生成验证码 + if ("math".equals(captchaType)) + { + String capText = captchaProducerMath.createText(); + capStr = capText.substring(0, capText.lastIndexOf("@")); + code = capText.substring(capText.lastIndexOf("@") + 1); + image = captchaProducerMath.createImage(capStr); + } + else if ("char".equals(captchaType)) + { + capStr = code = captchaProducer.createText(); + image = captchaProducer.createImage(capStr); + } + + redisService.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + // 转换流信息写出 + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + try + { + ImageIO.write(image, "jpg", os); + } + catch (IOException e) + { + return AjaxResult.error(e.getMessage()); + } + + ajax.put("uuid", uuid); + ajax.put("img", Base64.encode(os.toByteArray())); + return ajax; + } + + /** + * 校验验证码 + */ + @Override + public void checkCaptcha(String code, String uuid) throws CaptchaException + { + if (StringUtils.isEmpty(code)) + { + throw new CaptchaException("验证码不能为空"); + } + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisService.getCacheObject(verifyKey); + if (captcha == null) + { + throw new CaptchaException("验证码已失效"); + } + redisService.deleteObject(verifyKey); + if (!code.equalsIgnoreCase(captcha)) + { + throw new CaptchaException("验证码错误"); + } + } +} diff --git a/storm-gateway/src/main/resources/banner.txt b/storm-gateway/src/main/resources/banner.txt new file mode 100644 index 0000000..ceced29 --- /dev/null +++ b/storm-gateway/src/main/resources/banner.txt @@ -0,0 +1,10 @@ +Spring Boot Version: ${spring-boot.version} +Spring Application Name: ${spring.application.name} + _ _ + (_) | | + _ __ _ _ ___ _ _ _ ______ __ _ __ _ | |_ ___ __ __ __ _ _ _ +| '__|| | | | / _ \ | | | || ||______| / _` | / _` || __| / _ \\ \ /\ / / / _` || | | | +| | | |_| || (_) || |_| || | | (_| || (_| || |_ | __/ \ V V / | (_| || |_| | +|_| \__,_| \___/ \__, ||_| \__, | \__,_| \__| \___| \_/\_/ \__,_| \__, | + __/ | __/ | __/ | + |___/ |___/ |___/ \ No newline at end of file diff --git a/storm-gateway/src/main/resources/bootstrap.yml b/storm-gateway/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..1fbd8bf --- /dev/null +++ b/storm-gateway/src/main/resources/bootstrap.yml @@ -0,0 +1,47 @@ +# Tomcat +server: + port: 8080 + +# Spring +spring: + application: + # 应用名称 + name: storm-gateway + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 127.0.0.1:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: 127.0.0.1:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} + sentinel: + # 取消控制台懒加载 + eager: true + transport: + # 控制台地址 + dashboard: storm-nacos:8718 + # nacos配置持久化 + datasource: + ds1: + nacos: + server-addr: storm-nacos:8848 + dataId: sentinel-storm-gateway + groupId: DEFAULT_GROUP + data-type: json + rule-type: gw-flow diff --git a/storm-gateway/src/main/resources/bootstrap.yml01 b/storm-gateway/src/main/resources/bootstrap.yml01 new file mode 100644 index 0000000..827ca9e --- /dev/null +++ b/storm-gateway/src/main/resources/bootstrap.yml01 @@ -0,0 +1,47 @@ +# Tomcat +server: + port: 8080 + +# Spring +spring: + application: + # 应用名称 + name: storm-gateway + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} + sentinel: + # 取消控制台懒加载 + eager: true + transport: + # 控制台地址 + dashboard: 192.168.48.129:8718 + # nacos配置持久化 + datasource: + ds1: + nacos: + server-addr: 192.168.48.129:8848 + dataId: sentinel-storm-gateway + groupId: DEFAULT_GROUP + data-type: json + rule-type: gw-flow diff --git a/storm-gateway/src/main/resources/logback.xml b/storm-gateway/src/main/resources/logback.xml new file mode 100644 index 0000000..f07e294 --- /dev/null +++ b/storm-gateway/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/storm-modules/pom.xml b/storm-modules/pom.xml new file mode 100644 index 0000000..91ed913 --- /dev/null +++ b/storm-modules/pom.xml @@ -0,0 +1,25 @@ + + + + com.storm + storm + 3.6.6 + + 4.0.0 + + + storm-system + storm-gen + storm-job + storm-file + + + storm-modules + pom + + + storm-modules业务模块 + + + diff --git a/storm-modules/storm-file/pom.xml b/storm-modules/storm-file/pom.xml new file mode 100644 index 0000000..b6966c9 --- /dev/null +++ b/storm-modules/storm-file/pom.xml @@ -0,0 +1,101 @@ + + + + com.storm + storm-modules + 3.6.6 + + 4.0.0 + + storm-modules-file + + + storm-modules-file文件服务 + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.github.tobato + fastdfs-client + + + + + io.minio + minio + ${minio.version} + + + + + com.storm + storm-api-system + + + + + org.projectlombok + lombok + + + + + com.storm + storm-common-oss + 3.6.6 + + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/storm-modules/storm-file/src/main/java/com/storm/file/FileApplication.java b/storm-modules/storm-file/src/main/java/com/storm/file/FileApplication.java new file mode 100644 index 0000000..f3ea3a1 --- /dev/null +++ b/storm-modules/storm-file/src/main/java/com/storm/file/FileApplication.java @@ -0,0 +1,44 @@ +package com.storm.file; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.env.Environment; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 文件服务 + * + * @author ruoyi + */ +@Slf4j +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class }) +public class FileApplication +{ + public static void main(String[] args) throws UnknownHostException { + SpringApplication app = new SpringApplicationBuilder(FileApplication.class).build(args); + Environment env = app.run(args).getEnvironment(); + String protocol = "http"; + + if (env.getProperty("server.ssl.key-store") != null) { + protocol = "https"; + } + log.info("--/\n---------------------------------------------------------------------------------------\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\t{}://localhost:{}\n\t" + + "External: \t{}://{}:{}\n\t" + + "Profile(s): \t{}" + + "\n---------------------------------------------------------------------------------------", + env.getProperty("spring.application.name"), + protocol, + env.getProperty("server.port"), + protocol, + InetAddress.getLocalHost().getHostAddress(), + env.getProperty("server.port"), + env.getActiveProfiles()); + } +} diff --git a/storm-modules/storm-file/src/main/java/com/storm/file/config/MinioConfig.java b/storm-modules/storm-file/src/main/java/com/storm/file/config/MinioConfig.java new file mode 100644 index 0000000..cf6cd1c --- /dev/null +++ b/storm-modules/storm-file/src/main/java/com/storm/file/config/MinioConfig.java @@ -0,0 +1,82 @@ +package com.storm.file.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import io.minio.MinioClient; + +/** + * Minio 配置信息 + * + * @author ruoyi + */ +@Configuration +@ConfigurationProperties(prefix = "minio") +public class MinioConfig +{ + /** + * 服务地址 + */ + private String url; + + /** + * 用户名 + */ + private String accessKey; + + /** + * 密码 + */ + private String secretKey; + + /** + * 存储桶名称 + */ + private String bucketName; + + public String getUrl() + { + return url; + } + + public void setUrl(String url) + { + this.url = url; + } + + public String getAccessKey() + { + return accessKey; + } + + public void setAccessKey(String accessKey) + { + this.accessKey = accessKey; + } + + public String getSecretKey() + { + return secretKey; + } + + public void setSecretKey(String secretKey) + { + this.secretKey = secretKey; + } + + public String getBucketName() + { + return bucketName; + } + + public void setBucketName(String bucketName) + { + this.bucketName = bucketName; + } + + @Bean + public MinioClient getMinioClient() + { + return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build(); + } +} diff --git a/storm-modules/storm-file/src/main/java/com/storm/file/config/ResourcesConfig.java b/storm-modules/storm-file/src/main/java/com/storm/file/config/ResourcesConfig.java new file mode 100644 index 0000000..4eb967f --- /dev/null +++ b/storm-modules/storm-file/src/main/java/com/storm/file/config/ResourcesConfig.java @@ -0,0 +1,50 @@ +package com.storm.file.config; + +import java.io.File; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 通用映射配置 + * + * @author ruoyi + */ +@Configuration +public class ResourcesConfig implements WebMvcConfigurer +{ + /** + * 上传文件存储在本地的根路径 + */ + @Value("${file.path}") + private String localFilePath; + + /** + * 资源映射路径 前缀 + */ + @Value("${file.prefix}") + public String localFilePrefix; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) + { + /** 本地文件上传路径 */ + registry.addResourceHandler(localFilePrefix + "/**") + .addResourceLocations("file:" + localFilePath + File.separator); + } + + /** + * 开启跨域 + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + // 设置允许跨域的路由 + registry.addMapping(localFilePrefix + "/**") + // 设置允许跨域请求的域名 + .allowedOrigins("*") + // 设置允许的方法 + .allowedMethods("GET"); + } +} \ No newline at end of file diff --git a/storm-modules/storm-file/src/main/java/com/storm/file/controller/SysFileController.java b/storm-modules/storm-file/src/main/java/com/storm/file/controller/SysFileController.java new file mode 100644 index 0000000..c28ae90 --- /dev/null +++ b/storm-modules/storm-file/src/main/java/com/storm/file/controller/SysFileController.java @@ -0,0 +1,73 @@ +package com.storm.file.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.storm.common.core.domain.R; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.file.FileUtils; +import com.storm.file.service.ISysFileService; +import com.storm.system.api.domain.SysFile; + +/** + * 文件请求处理 + * + * @author ruoyi + */ +@RestController +public class SysFileController +{ + private static final Logger log = LoggerFactory.getLogger(SysFileController.class); + + @Autowired + private ISysFileService sysFileService; + + /** + * 文件上传请求 + */ + @PostMapping("upload") + public R upload(MultipartFile file) + { + try + { + // 上传并返回访问地址 + String url = sysFileService.uploadFile(file); + SysFile sysFile = new SysFile(); + sysFile.setName(FileUtils.getName(url)); + sysFile.setUrl(url); + log.info("上传文件成功。{}", url); + return R.ok(sysFile); + } + catch (Exception e) + { + log.error("上传文件失败", e); + return R.fail(e.getMessage()); + } + } + + /** + * 文件删除请求 + */ + @DeleteMapping("delete") + public R delete(String fileUrl) + { + try + { + if (!FileUtils.validateFilePath(fileUrl)) + { + throw new Exception(StringUtils.format("资源文件({})非法,不允许删除。 ", fileUrl)); + } + sysFileService.deleteFile(fileUrl); + return R.ok(); + } + catch (Exception e) + { + log.error("删除文件失败", e); + return R.fail(e.getMessage()); + } + } +} diff --git a/storm-modules/storm-file/src/main/java/com/storm/file/service/FastDfsSysFileServiceImpl.java b/storm-modules/storm-file/src/main/java/com/storm/file/service/FastDfsSysFileServiceImpl.java new file mode 100644 index 0000000..1271f8f --- /dev/null +++ b/storm-modules/storm-file/src/main/java/com/storm/file/service/FastDfsSysFileServiceImpl.java @@ -0,0 +1,76 @@ +package com.storm.file.service; + +import java.io.InputStream; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.nacos.common.utils.IoUtils; +import com.github.tobato.fastdfs.domain.fdfs.StorePath; +import com.github.tobato.fastdfs.service.FastFileStorageClient; +import com.storm.common.core.utils.file.FileTypeUtils; + +/** + * FastDFS 文件存储 + * + * @author ruoyi + */ +@Service +public class FastDfsSysFileServiceImpl implements ISysFileService +{ + /** + * 域名或本机访问地址 + */ + @Value("${fdfs.domain}") + public String domain; + + @Autowired + private FastFileStorageClient storageClient; + + /** + * FastDfs文件上传接口 + * + * @param file 上传的文件 + * @return 访问地址 + * @throws Exception + */ + @Override + public String uploadFile(MultipartFile file) throws Exception + { + InputStream inputStream = null; + try + { + inputStream = file.getInputStream(); + StorePath storePath = storageClient.uploadFile(inputStream, file.getSize(), FileTypeUtils.getExtension(file), null); + return domain + "/" + storePath.getFullPath(); + } + catch (Exception e) + { + throw new RuntimeException("FastDfs Failed to upload file", e); + } + finally + { + IoUtils.closeQuietly(inputStream); + } + } + + /** + * FastDFS文件删除接口 + * + * @param fileUrl 文件访问URL + * @throws Exception + */ + @Override + public void deleteFile(String fileUrl) throws Exception + { + try + { + StorePath storePath = StorePath.parseFromUrl(fileUrl); + storageClient.deleteFile(storePath.getGroup(), storePath.getPath()); + } + catch (Exception e) + { + throw new RuntimeException("FastDfs Failed to delete file: ", e); + } + } +} diff --git a/storm-modules/storm-file/src/main/java/com/storm/file/service/ISysFileService.java b/storm-modules/storm-file/src/main/java/com/storm/file/service/ISysFileService.java new file mode 100644 index 0000000..35cf7b6 --- /dev/null +++ b/storm-modules/storm-file/src/main/java/com/storm/file/service/ISysFileService.java @@ -0,0 +1,28 @@ +package com.storm.file.service; + +import org.springframework.web.multipart.MultipartFile; + +/** + * 文件上传接口 + * + * @author ruoyi + */ +public interface ISysFileService +{ + /** + * 文件上传接口 + * + * @param file 上传的文件 + * @return 访问地址 + * @throws Exception + */ + public String uploadFile(MultipartFile file) throws Exception; + + /** + * 文件删除接口 + * + * @param fileUrl 文件访问URL + * @throws Exception + */ + public void deleteFile(String fileUrl) throws Exception; +} diff --git a/storm-modules/storm-file/src/main/java/com/storm/file/service/LocalSysFileServiceImpl.java b/storm-modules/storm-file/src/main/java/com/storm/file/service/LocalSysFileServiceImpl.java new file mode 100644 index 0000000..e3ea8c8 --- /dev/null +++ b/storm-modules/storm-file/src/main/java/com/storm/file/service/LocalSysFileServiceImpl.java @@ -0,0 +1,64 @@ +package com.storm.file.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.file.FileUtils; +import com.storm.file.utils.FileUploadUtils; + +/** + * 本地文件存储 + * + * @author ruoyi + */ +@Service +public class LocalSysFileServiceImpl implements ISysFileService +{ + /** + * 资源映射路径 前缀 + */ + @Value("${file.prefix}") + public String localFilePrefix; + + /** + * 域名或本机访问地址 + */ + @Value("${file.domain}") + public String domain; + + /** + * 上传文件存储在本地的根路径 + */ + @Value("${file.path}") + private String localFilePath; + + /** + * 本地文件上传接口 + * + * @param file 上传的文件 + * @return 访问地址 + * @throws Exception + */ + @Override + public String uploadFile(MultipartFile file) throws Exception + { + String name = FileUploadUtils.upload(localFilePath, file); + String url = domain + localFilePrefix + name; + return url; + } + + /** + * 本地文件删除接口 + * + * @param fileUrl 文件访问URL + * @throws Exception + */ + @Override + public void deleteFile(String fileUrl) throws Exception + { + String localFile = StringUtils.substringAfter(fileUrl, localFilePrefix); + FileUtils.deleteFile(localFilePath + localFile); + } +} diff --git a/storm-modules/storm-file/src/main/java/com/storm/file/service/MinioSysFileServiceImpl.java b/storm-modules/storm-file/src/main/java/com/storm/file/service/MinioSysFileServiceImpl.java new file mode 100644 index 0000000..307f9ae --- /dev/null +++ b/storm-modules/storm-file/src/main/java/com/storm/file/service/MinioSysFileServiceImpl.java @@ -0,0 +1,82 @@ +package com.storm.file.service; + +import java.io.InputStream; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.nacos.common.utils.IoUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.file.config.MinioConfig; +import com.storm.file.utils.FileUploadUtils; +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import io.minio.RemoveObjectArgs; + +/** + * Minio 文件存储 + * + * @author ruoyi + */ +@Service +public class MinioSysFileServiceImpl implements ISysFileService +{ + @Autowired + private MinioConfig minioConfig; + + @Autowired + private MinioClient client; + + /** + * Minio文件上传接口 + * + * @param file 上传的文件 + * @return 访问地址 + * @throws Exception + */ + @Override + public String uploadFile(MultipartFile file) throws Exception + { + InputStream inputStream = null; + try + { + String fileName = FileUploadUtils.extractFilename(file); + inputStream = file.getInputStream(); + PutObjectArgs args = PutObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(fileName) + .stream(inputStream, file.getSize(), -1) + .contentType(file.getContentType()) + .build(); + client.putObject(args); + return minioConfig.getUrl() + "/" + minioConfig.getBucketName() + "/" + fileName; + } + catch (Exception e) + { + throw new RuntimeException("Minio Failed to upload file", e); + } + finally + { + IoUtils.closeQuietly(inputStream); + } + } + + /** + * Minio文件删除接口 + * + * @param fileUrl 文件访问URL + * @throws Exception + */ + @Override + public void deleteFile(String fileUrl) throws Exception + { + try + { + String minioFile = StringUtils.substringAfter(fileUrl, minioConfig.getBucketName()); + client.removeObject(RemoveObjectArgs.builder().bucket(minioConfig.getBucketName()).object(minioFile).build()); + } + catch (Exception e) + { + throw new RuntimeException("Minio Failed to delete file", e); + } + } +} diff --git a/storm-modules/storm-file/src/main/java/com/storm/file/service/impl/HuaweiObsFileServiceImpl.java b/storm-modules/storm-file/src/main/java/com/storm/file/service/impl/HuaweiObsFileServiceImpl.java new file mode 100644 index 0000000..15c8b77 --- /dev/null +++ b/storm-modules/storm-file/src/main/java/com/storm/file/service/impl/HuaweiObsFileServiceImpl.java @@ -0,0 +1,56 @@ +package com.storm.file.service.impl; + +import com.storm.common.core.exception.file.FileUploadException; +import com.storm.file.service.ISysFileService; +import com.storm.oss.service.ObsHuaweiFileStorageService; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @author DengAo + * @date 2025/8/25 16:39 + */ +@Primary +@Service +public class HuaweiObsFileServiceImpl implements ISysFileService { + + private final ObsHuaweiFileStorageService obsService; + + public HuaweiObsFileServiceImpl(ObsHuaweiFileStorageService obsService) { + this.obsService = obsService; + } + + + @Override + public String uploadFile(MultipartFile file) throws Exception { + try { + // 增强的空文件检测 + if (file == null || file.isEmpty() || + file.getOriginalFilename() == null || + file.getOriginalFilename().isEmpty() || + file.getSize() == 0) { + + throw new FileUploadException("上传的文件为空或无效"); + } + + // 额外检查文件内容(适用于某些特殊空文件) + if (file.getBytes().length == 0) { + throw new FileUploadException("上传的文件内容为空"); + } + + return obsService.store(file.getOriginalFilename(), file.getInputStream()); + + } catch (IOException e) { + throw new FileUploadException("文件读取失败: " + e.getMessage(), e); + } + } + + @Override + public void deleteFile(String fileUrl) throws Exception { + obsService.delete(fileUrl); + } +} diff --git a/storm-modules/storm-file/src/main/java/com/storm/file/utils/FileUploadUtils.java b/storm-modules/storm-file/src/main/java/com/storm/file/utils/FileUploadUtils.java new file mode 100644 index 0000000..574c0d1 --- /dev/null +++ b/storm-modules/storm-file/src/main/java/com/storm/file/utils/FileUploadUtils.java @@ -0,0 +1,185 @@ +package com.storm.file.utils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Objects; +import org.apache.commons.io.FilenameUtils; +import org.springframework.web.multipart.MultipartFile; +import com.storm.common.core.exception.file.FileException; +import com.storm.common.core.exception.file.FileNameLengthLimitExceededException; +import com.storm.common.core.exception.file.FileSizeLimitExceededException; +import com.storm.common.core.exception.file.InvalidExtensionException; +import com.storm.common.core.utils.DateUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.file.FileTypeUtils; +import com.storm.common.core.utils.file.MimeTypeUtils; +import com.storm.common.core.utils.uuid.Seq; + +/** + * 文件上传工具类 + * + * @author ruoyi + */ +public class FileUploadUtils +{ + /** + * 默认大小 50M + */ + public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024L; + + /** + * 默认的文件名最大长度 100 + */ + public static final int DEFAULT_FILE_NAME_LENGTH = 100; + + /** + * 根据文件路径上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @return 文件名称 + * @throws IOException + */ + public static final String upload(String baseDir, MultipartFile file) throws IOException + { + try + { + return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (FileException fe) + { + throw new IOException(fe.getDefaultMessage(), fe); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, + InvalidExtensionException + { + int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length(); + if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) + { + throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH); + } + + assertAllowed(file, allowedExtension); + + String fileName = extractFilename(file); + + String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath(); + file.transferTo(Paths.get(absPath)); + return getPathFileName(fileName); + } + + /** + * 编码文件名 + */ + public static final String extractFilename(MultipartFile file) + { + return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(), + FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), FileTypeUtils.getExtension(file)); + } + + private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException + { + File desc = new File(uploadDir + File.separator + fileName); + + if (!desc.exists()) + { + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + } + return desc.isAbsolute() ? desc : desc.getAbsoluteFile(); + } + + private static final String getPathFileName(String fileName) throws IOException + { + String pathFileName = "/" + fileName; + return pathFileName; + } + + /** + * 文件大小校验 + * + * @param file 上传的文件 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final void assertAllowed(MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, InvalidExtensionException + { + long size = file.getSize(); + if (size > DEFAULT_MAX_SIZE) + { + throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); + } + + String fileName = file.getOriginalFilename(); + String extension = FileTypeUtils.getExtension(file); + if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) + { + if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) + { + throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) + { + throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) + { + throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) + { + throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension, + fileName); + } + else + { + throw new InvalidExtensionException(allowedExtension, extension, fileName); + } + } + } + + /** + * 判断MIME类型是否是允许的MIME类型 + * + * @param extension 上传文件类型 + * @param allowedExtension 允许上传文件类型 + * @return true/false + */ + public static final boolean isAllowedExtension(String extension, String[] allowedExtension) + { + for (String str : allowedExtension) + { + if (str.equalsIgnoreCase(extension)) + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/storm-modules/storm-file/src/main/resources/banner.txt b/storm-modules/storm-file/src/main/resources/banner.txt new file mode 100644 index 0000000..27cacb9 --- /dev/null +++ b/storm-modules/storm-file/src/main/resources/banner.txt @@ -0,0 +1,10 @@ +Spring Boot Version: ${spring-boot.version} +Spring Application Name: ${spring.application.name} + _ __ _ _ + (_) / _|(_)| | + _ __ _ _ ___ _ _ _ ______ | |_ _ | | ___ +| '__|| | | | / _ \ | | | || ||______|| _|| || | / _ \ +| | | |_| || (_) || |_| || | | | | || || __/ +|_| \__,_| \___/ \__, ||_| |_| |_||_| \___| + __/ | + |___/ \ No newline at end of file diff --git a/storm-modules/storm-file/src/main/resources/bootstrap.yml b/storm-modules/storm-file/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..a505254 --- /dev/null +++ b/storm-modules/storm-file/src/main/resources/bootstrap.yml @@ -0,0 +1,32 @@ +# Tomcat +server: + port: 9300 + +# Spring +spring: + application: + # 应用名称 + name: storm-file + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: storm-nacos:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: storm-nacos:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} diff --git a/storm-modules/storm-file/src/main/resources/bootstrap.yml01 b/storm-modules/storm-file/src/main/resources/bootstrap.yml01 new file mode 100644 index 0000000..e7fad50 --- /dev/null +++ b/storm-modules/storm-file/src/main/resources/bootstrap.yml01 @@ -0,0 +1,32 @@ +# Tomcat +server: + port: 9300 + +# Spring +spring: + application: + # 应用名称 + name: storm-file + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} diff --git a/storm-modules/storm-file/src/main/resources/logback.xml b/storm-modules/storm-file/src/main/resources/logback.xml new file mode 100644 index 0000000..4b5b382 --- /dev/null +++ b/storm-modules/storm-file/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/storm-modules/storm-gen/pom.xml b/storm-modules/storm-gen/pom.xml new file mode 100644 index 0000000..3f18465 --- /dev/null +++ b/storm-modules/storm-gen/pom.xml @@ -0,0 +1,93 @@ + + + + com.storm + storm-modules + 3.6.6 + + 4.0.0 + + storm-modules-gen + + + storm-modules-gen代码生成 + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.apache.velocity + velocity-engine-core + + + + + com.mysql + mysql-connector-j + + + + + com.storm + storm-common-log + + + + + com.storm + storm-common-swagger + + + + + org.projectlombok + lombok + + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/GenApplication.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/GenApplication.java new file mode 100644 index 0000000..0340393 --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/GenApplication.java @@ -0,0 +1,46 @@ +package com.storm.gen; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import com.storm.common.security.annotation.EnableCustomConfig; +import com.storm.common.security.annotation.EnableRyFeignClients; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.env.Environment; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 代码生成 + * + * @author ruoyi + */ +@Slf4j +@EnableCustomConfig +@EnableRyFeignClients +@SpringBootApplication +public class GenApplication +{ + public static void main(String[] args) throws UnknownHostException { + SpringApplication app = new SpringApplicationBuilder(GenApplication.class).build(args); + Environment env = app.run(args).getEnvironment(); + String protocol = "http"; + if (env.getProperty("server.ssl.key-store") != null) { + protocol = "https"; + } + log.info("--/\n---------------------------------------------------------------------------------------\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\t{}://localhost:{}\n\t" + + "External: \t{}://{}:{}\n\t" + + "Profile(s): \t{}" + + "\n---------------------------------------------------------------------------------------", + env.getProperty("spring.application.name"), + protocol, + env.getProperty("server.port"), + protocol, + InetAddress.getLocalHost().getHostAddress(), + env.getProperty("server.port"), + env.getActiveProfiles()); + } +} diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/config/GenConfig.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/config/GenConfig.java new file mode 100644 index 0000000..8f91b2e --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/config/GenConfig.java @@ -0,0 +1,79 @@ +package com.storm.gen.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 代码生成相关配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "gen") +public class GenConfig +{ + /** 作者 */ + public static String author; + + /** 生成包路径 */ + public static String packageName; + + /** 自动去除表前缀 */ + public static boolean autoRemovePre; + + /** 表前缀 */ + public static String tablePrefix; + + /** 是否允许生成文件覆盖到本地(自定义路径) */ + public static boolean allowOverwrite; + + public static String getAuthor() + { + return author; + } + + public void setAuthor(String author) + { + GenConfig.author = author; + } + + public static String getPackageName() + { + return packageName; + } + + public void setPackageName(String packageName) + { + GenConfig.packageName = packageName; + } + + public static boolean getAutoRemovePre() + { + return autoRemovePre; + } + + public void setAutoRemovePre(boolean autoRemovePre) + { + GenConfig.autoRemovePre = autoRemovePre; + } + + public static String getTablePrefix() + { + return tablePrefix; + } + + public void setTablePrefix(String tablePrefix) + { + GenConfig.tablePrefix = tablePrefix; + } + + public static boolean isAllowOverwrite() + { + return allowOverwrite; + } + + public void setAllowOverwrite(boolean allowOverwrite) + { + GenConfig.allowOverwrite = allowOverwrite; + } +} diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/controller/GenController.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/controller/GenController.java new file mode 100644 index 0000000..2ff1f36 --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/controller/GenController.java @@ -0,0 +1,216 @@ +package com.storm.gen.controller; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.text.Convert; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.gen.config.GenConfig; +import com.storm.gen.domain.GenTable; +import com.storm.gen.domain.GenTableColumn; +import com.storm.gen.service.IGenTableColumnService; +import com.storm.gen.service.IGenTableService; + +/** + * 代码生成 操作处理 + * + * @author ruoyi + */ +@RequestMapping("/gen") +@RestController +public class GenController extends BaseController +{ + @Autowired + private IGenTableService genTableService; + + @Autowired + private IGenTableColumnService genTableColumnService; + + /** + * 查询代码生成列表 + */ + @RequiresPermissions("tool:gen:list") + @GetMapping("/list") + public TableDataInfo genList(GenTable genTable) + { + startPage(); + List list = genTableService.selectGenTableList(genTable); + return getDataTable(list); + } + + /** + * 获取代码生成信息 + */ + @RequiresPermissions("tool:gen:query") + @GetMapping(value = "/{tableId}") + public AjaxResult getInfo(@PathVariable Long tableId) + { + GenTable table = genTableService.selectGenTableById(tableId); + List tables = genTableService.selectGenTableAll(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + Map map = new HashMap(); + map.put("info", table); + map.put("rows", list); + map.put("tables", tables); + return success(map); + } + + /** + * 查询数据库列表 + */ + @RequiresPermissions("tool:gen:list") + @GetMapping("/db/list") + public TableDataInfo dataList(GenTable genTable) + { + startPage(); + List list = genTableService.selectDbTableList(genTable); + return getDataTable(list); + } + + /** + * 查询数据表字段列表 + */ + @GetMapping(value = "/column/{tableId}") + public TableDataInfo columnList(Long tableId) + { + TableDataInfo dataInfo = new TableDataInfo(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + dataInfo.setRows(list); + dataInfo.setTotal(list.size()); + return dataInfo; + } + + /** + * 导入表结构(保存) + */ + @RequiresPermissions("tool:gen:import") + @Log(title = "代码生成", businessType = BusinessType.IMPORT) + @PostMapping("/importTable") + public AjaxResult importTableSave(String tables) + { + String[] tableNames = Convert.toStrArray(tables); + // 查询表信息 + List tableList = genTableService.selectDbTableListByNames(tableNames); + genTableService.importGenTable(tableList); + return success(); + } + + /** + * 修改保存代码生成业务 + */ + @RequiresPermissions("tool:gen:edit") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult editSave(@Validated @RequestBody GenTable genTable) + { + genTableService.validateEdit(genTable); + genTableService.updateGenTable(genTable); + return success(); + } + + /** + * 删除代码生成 + */ + @RequiresPermissions("tool:gen:remove") + @Log(title = "代码生成", businessType = BusinessType.DELETE) + @DeleteMapping("/{tableIds}") + public AjaxResult remove(@PathVariable Long[] tableIds) + { + genTableService.deleteGenTableByIds(tableIds); + return success(); + } + + /** + * 预览代码 + */ + @RequiresPermissions("tool:gen:preview") + @GetMapping("/preview/{tableId}") + public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException + { + Map dataMap = genTableService.previewCode(tableId); + return success(dataMap); + } + + /** + * 生成代码(下载方式) + */ + @RequiresPermissions("tool:gen:code") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/download/{tableName}") + public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException + { + byte[] data = genTableService.downloadCode(tableName); + genCode(response, data); + } + + /** + * 生成代码(自定义路径) + */ + @RequiresPermissions("tool:gen:code") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/genCode/{tableName}") + public AjaxResult genCode(@PathVariable("tableName") String tableName) + { + if (!GenConfig.isAllowOverwrite()) + { + return AjaxResult.error("【系统预设】不允许生成文件覆盖到本地"); + } + genTableService.generatorCode(tableName); + return success(); + } + + /** + * 同步数据库 + */ + @RequiresPermissions("tool:gen:edit") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @GetMapping("/synchDb/{tableName}") + public AjaxResult synchDb(@PathVariable("tableName") String tableName) + { + genTableService.synchDb(tableName); + return success(); + } + + /** + * 批量生成代码 + */ + @RequiresPermissions("tool:gen:code") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/batchGenCode") + public void batchGenCode(HttpServletResponse response, String tables) throws IOException + { + String[] tableNames = Convert.toStrArray(tables); + byte[] data = genTableService.downloadCode(tableNames); + genCode(response, data); + } + + /** + * 生成zip文件 + */ + private void genCode(HttpServletResponse response, byte[] data) throws IOException + { + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=\"ruoyi.zip\""); + response.addHeader("Content-Length", "" + data.length); + response.setContentType("application/octet-stream; charset=UTF-8"); + IOUtils.write(data, response.getOutputStream()); + } +} diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/domain/GenTable.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/domain/GenTable.java new file mode 100644 index 0000000..d65f444 --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/domain/GenTable.java @@ -0,0 +1,383 @@ +package com.storm.gen.domain; + +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import org.apache.commons.lang3.ArrayUtils; +import com.storm.common.core.constant.GenConstants; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 业务表 gen_table + * + * @author ruoyi + */ +public class GenTable extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long tableId; + + /** 表名称 */ + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** 表描述 */ + @NotBlank(message = "表描述不能为空") + private String tableComment; + + /** 关联父表的表名 */ + private String subTableName; + + /** 本表关联父表的外键名 */ + private String subTableFkName; + + /** 实体类名称(首字母大写) */ + @NotBlank(message = "实体类名称不能为空") + private String className; + + /** 使用的模板(crud单表操作 tree树表操作 sub主子表操作) */ + private String tplCategory; + + /** 前端类型(element-ui模版 element-plus模版) */ + private String tplWebType; + + /** 生成包路径 */ + @NotBlank(message = "生成包路径不能为空") + private String packageName; + + /** 生成模块名 */ + @NotBlank(message = "生成模块名不能为空") + private String moduleName; + + /** 生成业务名 */ + @NotBlank(message = "生成业务名不能为空") + private String businessName; + + /** 生成功能名 */ + @NotBlank(message = "生成功能名不能为空") + private String functionName; + + /** 生成作者 */ + @NotBlank(message = "作者不能为空") + private String functionAuthor; + + /** 生成代码方式(0zip压缩包 1自定义路径) */ + private String genType; + + /** 生成路径(不填默认项目路径) */ + private String genPath; + + /** 主键信息 */ + private GenTableColumn pkColumn; + + /** 子表信息 */ + private GenTable subTable; + + /** 表列信息 */ + @Valid + private List columns; + + /** 其它生成选项 */ + private String options; + + /** 树编码字段 */ + private String treeCode; + + /** 树父编码字段 */ + private String treeParentCode; + + /** 树名称字段 */ + private String treeName; + + /** 上级菜单ID字段 */ + private Long parentMenuId; + + /** 上级菜单名称字段 */ + private String parentMenuName; + + public Long getTableId() + { + return tableId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public String getTableName() + { + return tableName; + } + + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + public String getTableComment() + { + return tableComment; + } + + public void setTableComment(String tableComment) + { + this.tableComment = tableComment; + } + + public String getSubTableName() + { + return subTableName; + } + + public void setSubTableName(String subTableName) + { + this.subTableName = subTableName; + } + + public String getSubTableFkName() + { + return subTableFkName; + } + + public void setSubTableFkName(String subTableFkName) + { + this.subTableFkName = subTableFkName; + } + + public String getClassName() + { + return className; + } + + public void setClassName(String className) + { + this.className = className; + } + + public String getTplCategory() + { + return tplCategory; + } + + public void setTplCategory(String tplCategory) + { + this.tplCategory = tplCategory; + } + + public String getTplWebType() + { + return tplWebType; + } + + public void setTplWebType(String tplWebType) + { + this.tplWebType = tplWebType; + } + + public String getPackageName() + { + return packageName; + } + + public void setPackageName(String packageName) + { + this.packageName = packageName; + } + + public String getModuleName() + { + return moduleName; + } + + public void setModuleName(String moduleName) + { + this.moduleName = moduleName; + } + + public String getBusinessName() + { + return businessName; + } + + public void setBusinessName(String businessName) + { + this.businessName = businessName; + } + + public String getFunctionName() + { + return functionName; + } + + public void setFunctionName(String functionName) + { + this.functionName = functionName; + } + + public String getFunctionAuthor() + { + return functionAuthor; + } + + public void setFunctionAuthor(String functionAuthor) + { + this.functionAuthor = functionAuthor; + } + + public String getGenType() + { + return genType; + } + + public void setGenType(String genType) + { + this.genType = genType; + } + + public String getGenPath() + { + return genPath; + } + + public void setGenPath(String genPath) + { + this.genPath = genPath; + } + + public GenTableColumn getPkColumn() + { + return pkColumn; + } + + public void setPkColumn(GenTableColumn pkColumn) + { + this.pkColumn = pkColumn; + } + + public GenTable getSubTable() + { + return subTable; + } + + public void setSubTable(GenTable subTable) + { + this.subTable = subTable; + } + public List getColumns() + { + return columns; + } + + public void setColumns(List columns) + { + this.columns = columns; + } + + public String getOptions() + { + return options; + } + + public void setOptions(String options) + { + this.options = options; + } + + public String getTreeCode() + { + return treeCode; + } + + public void setTreeCode(String treeCode) + { + this.treeCode = treeCode; + } + + public String getTreeParentCode() + { + return treeParentCode; + } + + public void setTreeParentCode(String treeParentCode) + { + this.treeParentCode = treeParentCode; + } + + public String getTreeName() + { + return treeName; + } + + public void setTreeName(String treeName) + { + this.treeName = treeName; + } + + public Long getParentMenuId() + { + return parentMenuId; + } + + public void setParentMenuId(Long parentMenuId) + { + this.parentMenuId = parentMenuId; + } + + public String getParentMenuName() + { + return parentMenuName; + } + + public void setParentMenuName(String parentMenuName) + { + this.parentMenuName = parentMenuName; + } + + public boolean isSub() + { + return isSub(this.tplCategory); + } + + public static boolean isSub(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_SUB, tplCategory); + } + public boolean isTree() + { + return isTree(this.tplCategory); + } + + public static boolean isTree(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_TREE, tplCategory); + } + + public boolean isCrud() + { + return isCrud(this.tplCategory); + } + + public static boolean isCrud(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_CRUD, tplCategory); + } + + public boolean isSuperColumn(String javaField) + { + return isSuperColumn(this.tplCategory, javaField); + } + + public static boolean isSuperColumn(String tplCategory, String javaField) + { + if (isTree(tplCategory)) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + ArrayUtils.addAll(GenConstants.TREE_ENTITY, GenConstants.BASE_ENTITY)); + } + return StringUtils.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY); + } +} diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/domain/GenTableColumn.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/domain/GenTableColumn.java new file mode 100644 index 0000000..d0b5b27 --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/domain/GenTableColumn.java @@ -0,0 +1,373 @@ +package com.storm.gen.domain; + +import javax.validation.constraints.NotBlank; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 代码生成业务字段表 gen_table_column + * + * @author ruoyi + */ +public class GenTableColumn extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long columnId; + + /** 归属表编号 */ + private Long tableId; + + /** 列名称 */ + private String columnName; + + /** 列描述 */ + private String columnComment; + + /** 列类型 */ + private String columnType; + + /** JAVA类型 */ + private String javaType; + + /** JAVA字段名 */ + @NotBlank(message = "Java属性不能为空") + private String javaField; + + /** 是否主键(1是) */ + private String isPk; + + /** 是否自增(1是) */ + private String isIncrement; + + /** 是否必填(1是) */ + private String isRequired; + + /** 是否为插入字段(1是) */ + private String isInsert; + + /** 是否编辑字段(1是) */ + private String isEdit; + + /** 是否列表字段(1是) */ + private String isList; + + /** 是否查询字段(1是) */ + private String isQuery; + + /** 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围) */ + private String queryType; + + /** 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件) */ + private String htmlType; + + /** 字典类型 */ + private String dictType; + + /** 排序 */ + private Integer sort; + + public void setColumnId(Long columnId) + { + this.columnId = columnId; + } + + public Long getColumnId() + { + return columnId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public Long getTableId() + { + return tableId; + } + + public void setColumnName(String columnName) + { + this.columnName = columnName; + } + + public String getColumnName() + { + return columnName; + } + + public void setColumnComment(String columnComment) + { + this.columnComment = columnComment; + } + + public String getColumnComment() + { + return columnComment; + } + + public void setColumnType(String columnType) + { + this.columnType = columnType; + } + + public String getColumnType() + { + return columnType; + } + + public void setJavaType(String javaType) + { + this.javaType = javaType; + } + + public String getJavaType() + { + return javaType; + } + + public void setJavaField(String javaField) + { + this.javaField = javaField; + } + + public String getJavaField() + { + return javaField; + } + + public String getCapJavaField() + { + return StringUtils.capitalize(javaField); + } + + public void setIsPk(String isPk) + { + this.isPk = isPk; + } + + public String getIsPk() + { + return isPk; + } + + public boolean isPk() + { + return isPk(this.isPk); + } + + public boolean isPk(String isPk) + { + return isPk != null && StringUtils.equals("1", isPk); + } + + public String getIsIncrement() + { + return isIncrement; + } + + public void setIsIncrement(String isIncrement) + { + this.isIncrement = isIncrement; + } + + public boolean isIncrement() + { + return isIncrement(this.isIncrement); + } + + public boolean isIncrement(String isIncrement) + { + return isIncrement != null && StringUtils.equals("1", isIncrement); + } + + public void setIsRequired(String isRequired) + { + this.isRequired = isRequired; + } + + public String getIsRequired() + { + return isRequired; + } + + public boolean isRequired() + { + return isRequired(this.isRequired); + } + + public boolean isRequired(String isRequired) + { + return isRequired != null && StringUtils.equals("1", isRequired); + } + + public void setIsInsert(String isInsert) + { + this.isInsert = isInsert; + } + + public String getIsInsert() + { + return isInsert; + } + + public boolean isInsert() + { + return isInsert(this.isInsert); + } + + public boolean isInsert(String isInsert) + { + return isInsert != null && StringUtils.equals("1", isInsert); + } + + public void setIsEdit(String isEdit) + { + this.isEdit = isEdit; + } + + public String getIsEdit() + { + return isEdit; + } + + public boolean isEdit() + { + return isInsert(this.isEdit); + } + + public boolean isEdit(String isEdit) + { + return isEdit != null && StringUtils.equals("1", isEdit); + } + + public void setIsList(String isList) + { + this.isList = isList; + } + + public String getIsList() + { + return isList; + } + + public boolean isList() + { + return isList(this.isList); + } + + public boolean isList(String isList) + { + return isList != null && StringUtils.equals("1", isList); + } + + public void setIsQuery(String isQuery) + { + this.isQuery = isQuery; + } + + public String getIsQuery() + { + return isQuery; + } + + public boolean isQuery() + { + return isQuery(this.isQuery); + } + + public boolean isQuery(String isQuery) + { + return isQuery != null && StringUtils.equals("1", isQuery); + } + + public void setQueryType(String queryType) + { + this.queryType = queryType; + } + + public String getQueryType() + { + return queryType; + } + + public String getHtmlType() + { + return htmlType; + } + + public void setHtmlType(String htmlType) + { + this.htmlType = htmlType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getDictType() + { + return dictType; + } + + public void setSort(Integer sort) + { + this.sort = sort; + } + + public Integer getSort() + { + return sort; + } + + public boolean isSuperColumn() + { + return isSuperColumn(this.javaField); + } + + public static boolean isSuperColumn(String javaField) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + // BaseEntity + "createBy", "createTime", "updateBy", "updateTime", "remark", + // TreeEntity + "parentName", "parentId", "orderNum", "ancestors"); + } + + public boolean isUsableColumn() + { + return isUsableColumn(javaField); + } + + public static boolean isUsableColumn(String javaField) + { + // isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单 + return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark"); + } + + public String readConverterExp() + { + String remarks = StringUtils.substringBetween(this.columnComment, "(", ")"); + StringBuffer sb = new StringBuffer(); + if (StringUtils.isNotEmpty(remarks)) + { + for (String value : remarks.split(" ")) + { + if (StringUtils.isNotEmpty(value)) + { + Object startStr = value.subSequence(0, 1); + String endStr = value.substring(1); + sb.append("").append(startStr).append("=").append(endStr).append(","); + } + } + return sb.deleteCharAt(sb.length() - 1).toString(); + } + else + { + return this.columnComment; + } + } +} \ No newline at end of file diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/mapper/GenTableColumnMapper.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/mapper/GenTableColumnMapper.java new file mode 100644 index 0000000..3c4a882 --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/mapper/GenTableColumnMapper.java @@ -0,0 +1,60 @@ +package com.storm.gen.mapper; + +import java.util.List; +import com.storm.gen.domain.GenTableColumn; + +/** + * 业务字段 数据层 + * + * @author ruoyi + */ +public interface GenTableColumnMapper +{ + /** + * 根据表名称查询列信息 + * + * @param tableName 表名称 + * @return 列信息 + */ + public List selectDbTableColumnsByName(String tableName); + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段 + * + * @param genTableColumns 列数据 + * @return 结果 + */ + public int deleteGenTableColumns(List genTableColumns); + + /** + * 批量删除业务字段 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(Long[] ids); +} \ No newline at end of file diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/mapper/GenTableMapper.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/mapper/GenTableMapper.java new file mode 100644 index 0000000..2667d3d --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/mapper/GenTableMapper.java @@ -0,0 +1,83 @@ +package com.storm.gen.mapper; + +import java.util.List; +import com.storm.gen.domain.GenTable; + +/** + * 业务 数据层 + * + * @author ruoyi + */ +public interface GenTableMapper +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询表ID业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 查询表名称业务信息 + * + * @param tableName 表名称 + * @return 业务信息 + */ + public GenTable selectGenTableByName(String tableName); + + /** + * 新增业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int insertGenTable(GenTable genTable); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int updateGenTable(GenTable genTable); + + /** + * 批量删除业务 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableByIds(Long[] ids); +} \ No newline at end of file diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/service/GenTableColumnServiceImpl.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/service/GenTableColumnServiceImpl.java new file mode 100644 index 0000000..8ba4d87 --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/service/GenTableColumnServiceImpl.java @@ -0,0 +1,68 @@ +package com.storm.gen.service; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.common.core.text.Convert; +import com.storm.gen.domain.GenTableColumn; +import com.storm.gen.mapper.GenTableColumnMapper; + +/** + * 业务字段 服务层实现 + * + * @author ruoyi + */ +@Service +public class GenTableColumnServiceImpl implements IGenTableColumnService +{ + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + @Override + public List selectGenTableColumnListByTableId(Long tableId) + { + return genTableColumnMapper.selectGenTableColumnListByTableId(tableId); + } + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int insertGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.insertGenTableColumn(genTableColumn); + } + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int updateGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + + /** + * 删除业务字段对象 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteGenTableColumnByIds(String ids) + { + return genTableColumnMapper.deleteGenTableColumnByIds(Convert.toLongArray(ids)); + } +} \ No newline at end of file diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/service/GenTableServiceImpl.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/service/GenTableServiceImpl.java new file mode 100644 index 0000000..8416154 --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/service/GenTableServiceImpl.java @@ -0,0 +1,521 @@ +package com.storm.gen.service; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.storm.common.core.constant.Constants; +import com.storm.common.core.constant.GenConstants; +import com.storm.common.core.exception.ServiceException; +import com.storm.common.core.text.CharsetKit; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.gen.domain.GenTable; +import com.storm.gen.domain.GenTableColumn; +import com.storm.gen.mapper.GenTableColumnMapper; +import com.storm.gen.mapper.GenTableMapper; +import com.storm.gen.util.GenUtils; +import com.storm.gen.util.VelocityInitializer; +import com.storm.gen.util.VelocityUtils; + +/** + * 业务 服务层实现 + * + * @author ruoyi + */ +@Service +public class GenTableServiceImpl implements IGenTableService +{ + private static final Logger log = LoggerFactory.getLogger(GenTableServiceImpl.class); + + @Autowired + private GenTableMapper genTableMapper; + + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + @Override + public GenTable selectGenTableById(Long id) + { + GenTable genTable = genTableMapper.selectGenTableById(id); + setTableFromOptions(genTable); + return genTable; + } + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + @Override + public List selectGenTableList(GenTable genTable) + { + return genTableMapper.selectGenTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + @Override + public List selectDbTableList(GenTable genTable) + { + return genTableMapper.selectDbTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + @Override + public List selectDbTableListByNames(String[] tableNames) + { + return genTableMapper.selectDbTableListByNames(tableNames); + } + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + @Override + public List selectGenTableAll() + { + return genTableMapper.selectGenTableAll(); + } + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateGenTable(GenTable genTable) + { + String options = JSON.toJSONString(genTable.getParams()); + genTable.setOptions(options); + int row = genTableMapper.updateGenTable(genTable); + if (row > 0) + { + for (GenTableColumn genTableColumn : genTable.getColumns()) + { + genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + } + } + + /** + * 删除业务对象 + * + * @param tableIds 需要删除的数据ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteGenTableByIds(Long[] tableIds) + { + genTableMapper.deleteGenTableByIds(tableIds); + genTableColumnMapper.deleteGenTableColumnByIds(tableIds); + } + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void importGenTable(List tableList) + { + String operName = SecurityUtils.getUsername(); + try + { + for (GenTable table : tableList) + { + String tableName = table.getTableName(); + GenUtils.initTable(table, operName); + int row = genTableMapper.insertGenTable(table); + if (row > 0) + { + // 保存列信息 + List genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + for (GenTableColumn column : genTableColumns) + { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insertGenTableColumn(column); + } + } + } + } + catch (Exception e) + { + throw new ServiceException("导入失败:" + e.getMessage()); + } + } + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + @Override + public Map previewCode(Long tableId) + { + Map dataMap = new LinkedHashMap<>(); + // 查询表信息 + GenTable table = genTableMapper.selectGenTableById(tableId); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + dataMap.put(template, sw.toString()); + } + return dataMap; + } + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + @Override + public byte[] downloadCode(String tableName) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + generatorCode(tableName, zip); + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + */ + @Override + public void generatorCode(String tableName) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm")) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + String path = getGenPath(table, template); + FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8); + } + catch (IOException e) + { + throw new ServiceException("渲染模板失败,表名:" + table.getTableName()); + } + } + } + } + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void synchDb(String tableName) + { + GenTable table = genTableMapper.selectGenTableByName(tableName); + List tableColumns = table.getColumns(); + Map tableColumnMap = tableColumns.stream().collect(Collectors.toMap(GenTableColumn::getColumnName, Function.identity())); + + List dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + if (StringUtils.isEmpty(dbTableColumns)) + { + throw new ServiceException("同步数据失败,原表结构不存在"); + } + List dbTableColumnNames = dbTableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList()); + + dbTableColumns.forEach(column -> { + GenUtils.initColumnField(column, table); + if (tableColumnMap.containsKey(column.getColumnName())) + { + GenTableColumn prevColumn = tableColumnMap.get(column.getColumnName()); + column.setColumnId(prevColumn.getColumnId()); + if (column.isList()) + { + // 如果是列表,继续保留查询方式/字典类型选项 + column.setDictType(prevColumn.getDictType()); + column.setQueryType(prevColumn.getQueryType()); + } + if (StringUtils.isNotEmpty(prevColumn.getIsRequired()) && !column.isPk() + && (column.isInsert() || column.isEdit()) + && ((column.isUsableColumn()) || (!column.isSuperColumn()))) + { + // 如果是(新增/修改&非主键/非忽略及父属性),继续保留必填/显示类型选项 + column.setIsRequired(prevColumn.getIsRequired()); + column.setHtmlType(prevColumn.getHtmlType()); + } + genTableColumnMapper.updateGenTableColumn(column); + } + else + { + genTableColumnMapper.insertGenTableColumn(column); + } + }); + + List delColumns = tableColumns.stream().filter(column -> !dbTableColumnNames.contains(column.getColumnName())).collect(Collectors.toList()); + if (StringUtils.isNotEmpty(delColumns)) + { + genTableColumnMapper.deleteGenTableColumns(delColumns); + } + } + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + @Override + public byte[] downloadCode(String[] tableNames) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + for (String tableName : tableNames) + { + generatorCode(tableName, zip); + } + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 查询表信息并生成代码 + */ + private void generatorCode(String tableName, ZipOutputStream zip) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + // 添加到zip + zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table))); + IOUtils.write(sw.toString(), zip, Constants.UTF8); + IOUtils.closeQuietly(sw); + zip.flush(); + zip.closeEntry(); + } + catch (IOException e) + { + log.error("渲染模板失败,表名:" + table.getTableName(), e); + } + } + } + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + @Override + public void validateEdit(GenTable genTable) + { + if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) + { + String options = JSON.toJSONString(genTable.getParams()); + JSONObject paramsObj = JSON.parseObject(options); + if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_CODE))) + { + throw new ServiceException("树编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_PARENT_CODE))) + { + throw new ServiceException("树父编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_NAME))) + { + throw new ServiceException("树名称字段不能为空"); + } + } + else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())) + { + if (StringUtils.isEmpty(genTable.getSubTableName())) + { + throw new ServiceException("关联子表的表名不能为空"); + } + else if (StringUtils.isEmpty(genTable.getSubTableFkName())) + { + throw new ServiceException("子表关联的外键名不能为空"); + } + } + } + + /** + * 设置主键列信息 + * + * @param table 业务表信息 + */ + public void setPkColumn(GenTable table) + { + for (GenTableColumn column : table.getColumns()) + { + if (column.isPk()) + { + table.setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getPkColumn())) + { + table.setPkColumn(table.getColumns().get(0)); + } + if (GenConstants.TPL_SUB.equals(table.getTplCategory())) + { + for (GenTableColumn column : table.getSubTable().getColumns()) + { + if (column.isPk()) + { + table.getSubTable().setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getSubTable().getPkColumn())) + { + table.getSubTable().setPkColumn(table.getSubTable().getColumns().get(0)); + } + } + } + + /** + * 设置主子表信息 + * + * @param table 业务表信息 + */ + public void setSubTable(GenTable table) + { + String subTableName = table.getSubTableName(); + if (StringUtils.isNotEmpty(subTableName)) + { + table.setSubTable(genTableMapper.selectGenTableByName(subTableName)); + } + } + + /** + * 设置代码生成其他选项值 + * + * @param genTable 设置后的生成对象 + */ + public void setTableFromOptions(GenTable genTable) + { + JSONObject paramsObj = JSON.parseObject(genTable.getOptions()); + if (StringUtils.isNotNull(paramsObj)) + { + String treeCode = paramsObj.getString(GenConstants.TREE_CODE); + String treeParentCode = paramsObj.getString(GenConstants.TREE_PARENT_CODE); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + Long parentMenuId = paramsObj.getLongValue(GenConstants.PARENT_MENU_ID); + String parentMenuName = paramsObj.getString(GenConstants.PARENT_MENU_NAME); + + genTable.setTreeCode(treeCode); + genTable.setTreeParentCode(treeParentCode); + genTable.setTreeName(treeName); + genTable.setParentMenuId(parentMenuId); + genTable.setParentMenuName(parentMenuName); + } + } + + /** + * 获取代码生成地址 + * + * @param table 业务表信息 + * @param template 模板文件路径 + * @return 生成地址 + */ + public static String getGenPath(GenTable table, String template) + { + String genPath = table.getGenPath(); + if (StringUtils.equals(genPath, "/")) + { + return System.getProperty("user.dir") + File.separator + "src" + File.separator + VelocityUtils.getFileName(template, table); + } + return genPath + File.separator + VelocityUtils.getFileName(template, table); + } +} diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/service/IGenTableColumnService.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/service/IGenTableColumnService.java new file mode 100644 index 0000000..7ac0bfa --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/service/IGenTableColumnService.java @@ -0,0 +1,44 @@ +package com.storm.gen.service; + +import java.util.List; +import com.storm.gen.domain.GenTableColumn; + +/** + * 业务字段 服务层 + * + * @author ruoyi + */ +public interface IGenTableColumnService +{ + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(String ids); +} diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/service/IGenTableService.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/service/IGenTableService.java new file mode 100644 index 0000000..edcd0ba --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/service/IGenTableService.java @@ -0,0 +1,121 @@ +package com.storm.gen.service; + +import java.util.List; +import java.util.Map; +import com.storm.gen.domain.GenTable; + +/** + * 业务 服务层 + * + * @author ruoyi + */ +public interface IGenTableService +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public void updateGenTable(GenTable genTable); + + /** + * 删除业务信息 + * + * @param tableIds 需要删除的表数据ID + * @return 结果 + */ + public void deleteGenTableByIds(Long[] tableIds); + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + public void importGenTable(List tableList); + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + public Map previewCode(Long tableId); + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + public byte[] downloadCode(String tableName); + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + * @return 数据 + */ + public void generatorCode(String tableName); + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + public void synchDb(String tableName); + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + public byte[] downloadCode(String[] tableNames); + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + public void validateEdit(GenTable genTable); +} diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/util/GenUtils.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/util/GenUtils.java new file mode 100644 index 0000000..8eb0f80 --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/util/GenUtils.java @@ -0,0 +1,258 @@ +package com.storm.gen.util; + +import java.util.Arrays; +import org.apache.commons.lang3.RegExUtils; +import com.storm.common.core.constant.GenConstants; +import com.storm.common.core.utils.StringUtils; +import com.storm.gen.config.GenConfig; +import com.storm.gen.domain.GenTable; +import com.storm.gen.domain.GenTableColumn; + +/** + * 代码生成器 工具类 + * + * @author ruoyi + */ +public class GenUtils +{ + /** + * 初始化表信息 + */ + public static void initTable(GenTable genTable, String operName) + { + genTable.setClassName(convertClassName(genTable.getTableName())); + genTable.setPackageName(GenConfig.getPackageName()); + genTable.setModuleName(getModuleName(GenConfig.getPackageName())); + genTable.setBusinessName(getBusinessName(genTable.getTableName())); + genTable.setFunctionName(replaceText(genTable.getTableComment())); + genTable.setFunctionAuthor(GenConfig.getAuthor()); + genTable.setCreateBy(operName); + } + + /** + * 初始化列属性字段 + */ + public static void initColumnField(GenTableColumn column, GenTable table) + { + String dataType = getDbType(column.getColumnType()); + String columnName = column.getColumnName(); + column.setTableId(table.getTableId()); + column.setCreateBy(table.getCreateBy()); + // 设置java字段名 + column.setJavaField(StringUtils.toCamelCase(columnName)); + // 设置默认类型 + column.setJavaType(GenConstants.TYPE_STRING); + column.setQueryType(GenConstants.QUERY_EQ); + + if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) + { + // 字符串长度超过500设置为文本域 + Integer columnLength = getColumnLength(column.getColumnType()); + String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) ? GenConstants.HTML_TEXTAREA : GenConstants.HTML_INPUT; + column.setHtmlType(htmlType); + } + else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType)) + { + column.setJavaType(GenConstants.TYPE_LOCAL_DATE_TIME); + column.setHtmlType(GenConstants.HTML_DATETIME); + } + else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType)) + { + column.setHtmlType(GenConstants.HTML_INPUT); + + // 如果是浮点型 统一用BigDecimal + String[] str = StringUtils.split(StringUtils.substringBetween(column.getColumnType(), "(", ")"), ","); + if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0) + { + column.setJavaType(GenConstants.TYPE_BIGDECIMAL); + } + // 如果是整形 + else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10 + || GenConstants.MYSQL_TINYINT.equals(column.getColumnType()) || GenConstants.MYSQL_INT.equals(column.getColumnType())) + { + column.setJavaType(GenConstants.TYPE_INTEGER); + } + // 长整形 + else + { + column.setJavaType(GenConstants.TYPE_LONG); + } + } + + // 插入字段(默认所有字段都需要插入) + column.setIsInsert(GenConstants.REQUIRE); + + // 编辑字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk()) + { + column.setIsEdit(GenConstants.REQUIRE); + } + // 列表字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk()) + { + column.setIsList(GenConstants.REQUIRE); + } + // 查询字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk()) + { + column.setIsQuery(GenConstants.REQUIRE); + } + + // 查询字段类型 + if (StringUtils.endsWithIgnoreCase(columnName, "name")) + { + column.setQueryType(GenConstants.QUERY_LIKE); + } + // 状态字段设置单选框 + if (StringUtils.endsWithIgnoreCase(columnName, "status")) + { + column.setHtmlType(GenConstants.HTML_RADIO); + } + // 类型&性别字段设置下拉框 + else if (StringUtils.endsWithIgnoreCase(columnName, "type") + || StringUtils.endsWithIgnoreCase(columnName, "sex")) + { + column.setHtmlType(GenConstants.HTML_SELECT); + } + // 图片字段设置图片上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "image")) + { + column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD); + } + // 文件字段设置文件上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "file")) + { + column.setHtmlType(GenConstants.HTML_FILE_UPLOAD); + } + // 内容字段设置富文本控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "content")) + { + column.setHtmlType(GenConstants.HTML_EDITOR); + } + } + + /** + * 校验数组是否包含指定值 + * + * @param arr 数组 + * @param targetValue 值 + * @return 是否包含 + */ + public static boolean arraysContains(String[] arr, String targetValue) + { + return Arrays.asList(arr).contains(targetValue); + } + + /** + * 获取模块名 + * + * @param packageName 包名 + * @return 模块名 + */ + public static String getModuleName(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + int nameLength = packageName.length(); + return StringUtils.substring(packageName, lastIndex + 1, nameLength); + } + + /** + * 获取业务名 + * + * @param tableName 表名 + * @return 业务名 + */ + public static String getBusinessName(String tableName) + { + int lastIndex = tableName.lastIndexOf("_"); + int nameLength = tableName.length(); + return StringUtils.substring(tableName, lastIndex + 1, nameLength); + } + + /** + * 表名转换成Java类名 + * + * @param tableName 表名称 + * @return 类名 + */ + public static String convertClassName(String tableName) + { + boolean autoRemovePre = GenConfig.getAutoRemovePre(); + String tablePrefix = GenConfig.getTablePrefix(); + if (autoRemovePre && StringUtils.isNotEmpty(tablePrefix)) + { + String[] searchList = StringUtils.split(tablePrefix, ","); + tableName = replaceFirst(tableName, searchList); + } + return StringUtils.convertToCamelCase(tableName); + } + + /** + * 批量替换前缀 + * + * @param replacementm 替换值 + * @param searchList 替换列表 + * @return + */ + public static String replaceFirst(String replacementm, String[] searchList) + { + String text = replacementm; + for (String searchString : searchList) + { + if (replacementm.startsWith(searchString)) + { + text = replacementm.replaceFirst(searchString, ""); + break; + } + } + return text; + } + + /** + * 关键字替换 + * + * @param text 需要被替换的名字 + * @return 替换后的名字 + */ + public static String replaceText(String text) + { + return RegExUtils.replaceAll(text, "(?:表|若依)", ""); + } + + /** + * 获取数据库类型字段 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static String getDbType(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + return StringUtils.substringBefore(columnType, "("); + } + else + { + return columnType; + } + } + + /** + * 获取字段长度 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static Integer getColumnLength(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + String length = StringUtils.substringBetween(columnType, "(", ")"); + return Integer.valueOf(length); + } + else + { + return 0; + } + } +} diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/util/VelocityInitializer.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/util/VelocityInitializer.java new file mode 100644 index 0000000..fa88f60 --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/util/VelocityInitializer.java @@ -0,0 +1,34 @@ +package com.storm.gen.util; + +import java.util.Properties; +import org.apache.velocity.app.Velocity; +import com.storm.common.core.constant.Constants; + +/** + * VelocityEngine工厂 + * + * @author ruoyi + */ +public class VelocityInitializer +{ + /** + * 初始化vm方法 + */ + public static void initVelocity() + { + Properties p = new Properties(); + try + { + // 加载classpath目录下的vm文件 + p.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + // 定义字符集 + p.setProperty(Velocity.INPUT_ENCODING, Constants.UTF8); + // 初始化Velocity引擎,指定配置Properties + Velocity.init(p); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } +} diff --git a/storm-modules/storm-gen/src/main/java/com/storm/gen/util/VelocityUtils.java b/storm-modules/storm-gen/src/main/java/com/storm/gen/util/VelocityUtils.java new file mode 100644 index 0000000..6cfb5b2 --- /dev/null +++ b/storm-modules/storm-gen/src/main/java/com/storm/gen/util/VelocityUtils.java @@ -0,0 +1,415 @@ +package com.storm.gen.util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.velocity.VelocityContext; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.storm.common.core.constant.GenConstants; +import com.storm.common.core.utils.DateUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.gen.domain.GenTable; +import com.storm.gen.domain.GenTableColumn; + +/** + * 模板工具类 + * + * @author ruoyi + */ +public class VelocityUtils +{ + /** 项目空间路径 */ + private static final String PROJECT_PATH = "main/java"; + + /** mybatis空间路径 */ + private static final String MYBATIS_PATH = "main/resources/mapper"; + + /** 默认上级菜单,系统工具 */ + private static final String DEFAULT_PARENT_MENU_ID = "3"; + + /** + * 设置模板变量信息 + * + * @return 模板列表 + */ + public static VelocityContext prepareContext(GenTable genTable) + { + String moduleName = genTable.getModuleName(); + String businessName = genTable.getBusinessName(); + String packageName = genTable.getPackageName(); + String tplCategory = genTable.getTplCategory(); + String functionName = genTable.getFunctionName(); + + VelocityContext velocityContext = new VelocityContext(); + velocityContext.put("tplCategory", genTable.getTplCategory()); + velocityContext.put("tableName", genTable.getTableName()); + velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); + velocityContext.put("ClassName", genTable.getClassName()); + velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName())); + velocityContext.put("moduleName", genTable.getModuleName()); + velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName())); + velocityContext.put("businessName", genTable.getBusinessName()); + velocityContext.put("basePackage", getPackagePrefix(packageName)); + velocityContext.put("packageName", packageName); + velocityContext.put("author", genTable.getFunctionAuthor()); + velocityContext.put("datetime", DateUtils.getDate()); + velocityContext.put("pkColumn", genTable.getPkColumn()); + velocityContext.put("importList", getImportList(genTable)); + velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); + velocityContext.put("columns", genTable.getColumns()); + velocityContext.put("table", genTable); + velocityContext.put("dicts", getDicts(genTable)); + setMenuVelocityContext(velocityContext, genTable); + if (GenConstants.TPL_TREE.equals(tplCategory)) + { + setTreeVelocityContext(velocityContext, genTable); + } + if (GenConstants.TPL_SUB.equals(tplCategory)) + { + setSubVelocityContext(velocityContext, genTable); + } + return velocityContext; + } + + public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String parentMenuId = getParentMenuId(paramsObj); + context.put("parentMenuId", parentMenuId); + } + + public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeCode = getTreecode(paramsObj); + String treeParentCode = getTreeParentCode(paramsObj); + String treeName = getTreeName(paramsObj); + + context.put("treeCode", treeCode); + context.put("treeParentCode", treeParentCode); + context.put("treeName", treeName); + context.put("expandColumn", getExpandColumn(genTable)); + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + context.put("tree_parent_code", paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + context.put("tree_name", paramsObj.getString(GenConstants.TREE_NAME)); + } + } + + public static void setSubVelocityContext(VelocityContext context, GenTable genTable) + { + GenTable subTable = genTable.getSubTable(); + String subTableName = genTable.getSubTableName(); + String subTableFkName = genTable.getSubTableFkName(); + String subClassName = genTable.getSubTable().getClassName(); + String subTableFkClassName = StringUtils.convertToCamelCase(subTableFkName); + + context.put("subTable", subTable); + context.put("subTableName", subTableName); + context.put("subTableFkName", subTableFkName); + context.put("subTableFkClassName", subTableFkClassName); + context.put("subTableFkclassName", StringUtils.uncapitalize(subTableFkClassName)); + context.put("subClassName", subClassName); + context.put("subclassName", StringUtils.uncapitalize(subClassName)); + context.put("subImportList", getImportList(genTable.getSubTable())); + } + + /** + * 获取模板信息 + * @param tplCategory 生成的模板 + * @param tplWebType 前端类型 + * @return 模板列表 + */ + public static List getTemplateList(String tplCategory, String tplWebType) + { + String useWebType = "vm/vue"; + if ("element-plus".equals(tplWebType)) + { + useWebType = "vm/vue/v3"; + } + List templates = new ArrayList(); + templates.add("vm/java/domain.java.vm"); + templates.add("vm/java/mapper.java.vm"); + templates.add("vm/java/service.java.vm"); + templates.add("vm/java/serviceImpl.java.vm"); + templates.add("vm/java/controller.java.vm"); + templates.add("vm/xml/mapper.xml.vm"); + templates.add("vm/sql/sql.vm"); + templates.add("vm/js/api.js.vm"); + if (GenConstants.TPL_CRUD.equals(tplCategory)) + { + templates.add(useWebType + "/index.vue.vm"); + } + else if (GenConstants.TPL_TREE.equals(tplCategory)) + { + templates.add(useWebType + "/index-tree.vue.vm"); + } + else if (GenConstants.TPL_SUB.equals(tplCategory)) + { + templates.add(useWebType + "/index.vue.vm"); + templates.add("vm/java/sub-domain.java.vm"); + } + return templates; + } + + /** + * 获取文件名 + */ + public static String getFileName(String template, GenTable genTable) + { + // 文件名称 + String fileName = ""; + // 包路径 + String packageName = genTable.getPackageName(); + // 模块名 + String moduleName = genTable.getModuleName(); + // 大写类名 + String className = genTable.getClassName(); + // 业务名称 + String businessName = genTable.getBusinessName(); + + String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/"); + String mybatisPath = MYBATIS_PATH + "/" + moduleName; + String vuePath = "vue"; + + if (template.contains("domain.java.vm")) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, className); + } + if (template.contains("sub-domain.java.vm") && StringUtils.equals(GenConstants.TPL_SUB, genTable.getTplCategory())) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName()); + } + else if (template.contains("mapper.java.vm")) + { + fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className); + } + else if (template.contains("service.java.vm")) + { + fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className); + } + else if (template.contains("serviceImpl.java.vm")) + { + fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className); + } + else if (template.contains("controller.java.vm")) + { + fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className); + } + else if (template.contains("mapper.xml.vm")) + { + fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className); + } + else if (template.contains("sql.vm")) + { + fileName = businessName + "Menu.sql"; + } + else if (template.contains("api.js.vm")) + { + fileName = StringUtils.format("{}/api/{}/{}.js", vuePath, moduleName, businessName); + } + else if (template.contains("index.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + else if (template.contains("index-tree.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + return fileName; + } + + /** + * 获取包前缀 + * + * @param packageName 包名称 + * @return 包前缀名称 + */ + public static String getPackagePrefix(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + return StringUtils.substring(packageName, 0, lastIndex); + } + + /** + * 根据列类型获取导入包 + * + * @param genTable 业务表对象 + * @return 返回需要导入的包列表 + */ + public static HashSet getImportList(GenTable genTable) + { + List columns = genTable.getColumns(); + GenTable subGenTable = genTable.getSubTable(); + HashSet importList = new HashSet(); + if (StringUtils.isNotNull(subGenTable)) + { + importList.add("java.util.List"); + } + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) + { + importList.add("java.util.Date"); + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } + else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) + { + importList.add("java.math.BigDecimal"); + } + // 如果字段类型在前端选择的是LocalDateTime,则导入对应的包 + if (!column.isSuperColumn() && GenConstants.TYPE_LOCAL_DATE_TIME.equals(column.getJavaType())) + { + importList.add("java.time.LocalDateTime"); + // 导入这个是为了格式化日期 + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } + } + return importList; + } + + /** + * 根据列类型获取字典组 + * + * @param genTable 业务表对象 + * @return 返回字典组 + */ + public static String getDicts(GenTable genTable) + { + List columns = genTable.getColumns(); + Set dicts = new HashSet(); + addDicts(dicts, columns); + if (StringUtils.isNotNull(genTable.getSubTable())) + { + List subColumns = genTable.getSubTable().getColumns(); + addDicts(dicts, subColumns); + } + return StringUtils.join(dicts, ", "); + } + + /** + * 添加字典列表 + * + * @param dicts 字典列表 + * @param columns 列集合 + */ + public static void addDicts(Set dicts, List columns) + { + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny( + column.getHtmlType(), + new String[] { GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX })) + { + dicts.add("'" + column.getDictType() + "'"); + } + } + } + + /** + * 获取权限前缀 + * + * @param moduleName 模块名称 + * @param businessName 业务名称 + * @return 返回权限前缀 + */ + public static String getPermissionPrefix(String moduleName, String businessName) + { + return StringUtils.format("{}:{}", moduleName, businessName); + } + + /** + * 获取上级菜单ID字段 + * + * @param paramsObj 生成其他选项 + * @return 上级菜单ID字段 + */ + public static String getParentMenuId(JSONObject paramsObj) + { + if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID) + && StringUtils.isNotEmpty(paramsObj.getString(GenConstants.PARENT_MENU_ID))) + { + return paramsObj.getString(GenConstants.PARENT_MENU_ID); + } + return DEFAULT_PARENT_MENU_ID; + } + + /** + * 获取树编码 + * + * @param paramsObj 生成其他选项 + * @return 树编码 + */ + public static String getTreecode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树父编码 + * + * @param paramsObj 生成其他选项 + * @return 树父编码 + */ + public static String getTreeParentCode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树名称 + * + * @param paramsObj 生成其他选项 + * @return 树名称 + */ + public static String getTreeName(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_NAME)); + } + return StringUtils.EMPTY; + } + + /** + * 获取需要在哪一列上面显示展开按钮 + * + * @param genTable 业务表对象 + * @return 展开按钮列序号 + */ + public static int getExpandColumn(GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + int num = 0; + for (GenTableColumn column : genTable.getColumns()) + { + if (column.isList()) + { + num++; + String columnName = column.getColumnName(); + if (columnName.equals(treeName)) + { + break; + } + } + } + return num; + } +} diff --git a/storm-modules/storm-gen/src/main/resources/applocaltion.yml b/storm-modules/storm-gen/src/main/resources/applocaltion.yml new file mode 100644 index 0000000..b037b1e --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/applocaltion.yml @@ -0,0 +1,12 @@ +# mybatis-plus配置 +mybatis-plus: + # 搜索指定包别名 + typeAliasesPackage: com.storm.gen.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath:mapper/**/*Mapper.xml + # 全局配置 + global-config: + db-config: + id-type: auto #id生成策略为自增 + configuration: + map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名 diff --git a/storm-modules/storm-gen/src/main/resources/banner.txt b/storm-modules/storm-gen/src/main/resources/banner.txt new file mode 100644 index 0000000..05f528c --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/banner.txt @@ -0,0 +1,10 @@ +Spring Boot Version: ${spring-boot.version} +Spring Application Name: ${spring.application.name} + _ + (_) + _ __ _ _ ___ _ _ _ ______ __ _ ___ _ __ +| '__|| | | | / _ \ | | | || ||______| / _` | / _ \| '_ \ +| | | |_| || (_) || |_| || | | (_| || __/| | | | +|_| \__,_| \___/ \__, ||_| \__, | \___||_| |_| + __/ | __/ | + |___/ |___/ \ No newline at end of file diff --git a/storm-modules/storm-gen/src/main/resources/bootstrap.yml b/storm-modules/storm-gen/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..7dce1a9 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/bootstrap.yml @@ -0,0 +1,32 @@ +# Tomcat +server: + port: 9202 + +# Spring +spring: + application: + # 应用名称 + name: storm-gen + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 127.0.0.1:8848 + username: nacos + password: + namespace: public + config: + # 配置中心地址 + server-addr: 127.0.0.1:8848 + username: nacos + password: + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} diff --git a/storm-modules/storm-gen/src/main/resources/logback.xml b/storm-modules/storm-gen/src/main/resources/logback.xml new file mode 100644 index 0000000..0c6fd90 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/storm-modules/storm-gen/src/main/resources/mapper/generator/GenTableColumnMapper.xml b/storm-modules/storm-gen/src/main/resources/mapper/generator/GenTableColumnMapper.xml new file mode 100644 index 0000000..941686e --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/mapper/generator/GenTableColumnMapper.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select column_id, table_id, column_name, column_comment, column_type, java_type, java_field, is_pk, is_increment, is_required, is_insert, is_edit, is_list, is_query, query_type, html_type, dict_type, sort, create_by, create_time, update_by, update_time from gen_table_column + + + + + + + + insert into gen_table_column ( + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time + )values( + #{tableId}, + #{columnName}, + #{columnComment}, + #{columnType}, + #{javaType}, + #{javaField}, + #{isPk}, + #{isIncrement}, + #{isRequired}, + #{isInsert}, + #{isEdit}, + #{isList}, + #{isQuery}, + #{queryType}, + #{htmlType}, + #{dictType}, + #{sort}, + #{createBy}, + sysdate() + ) + + + + update gen_table_column + + column_comment = #{columnComment}, + java_type = #{javaType}, + java_field = #{javaField}, + is_insert = #{isInsert}, + is_edit = #{isEdit}, + is_list = #{isList}, + is_query = #{isQuery}, + is_required = #{isRequired}, + query_type = #{queryType}, + html_type = #{htmlType}, + dict_type = #{dictType}, + sort = #{sort}, + update_by = #{updateBy}, + update_time = sysdate() + + where column_id = #{columnId} + + + + delete from gen_table_column where table_id in + + #{tableId} + + + + + delete from gen_table_column where column_id in + + #{item.columnId} + + + + \ No newline at end of file diff --git a/storm-modules/storm-gen/src/main/resources/mapper/generator/GenTableMapper.xml b/storm-modules/storm-gen/src/main/resources/mapper/generator/GenTableMapper.xml new file mode 100644 index 0000000..dc44be7 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/mapper/generator/GenTableMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, class_name, tpl_category, tpl_web_type, package_name, module_name, business_name, function_name, function_author, gen_type, gen_path, options, create_by, create_time, update_by, update_time, remark from gen_table + + + + + + + + + + + + + + + + + + insert into gen_table ( + table_name, + table_comment, + class_name, + tpl_category, + tpl_web_type, + package_name, + module_name, + business_name, + function_name, + function_author, + gen_type, + gen_path, + remark, + create_by, + create_time + )values( + #{tableName}, + #{tableComment}, + #{className}, + #{tplCategory}, + #{tplWebType}, + #{packageName}, + #{moduleName}, + #{businessName}, + #{functionName}, + #{functionAuthor}, + #{genType}, + #{genPath}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update gen_table + + table_name = #{tableName}, + table_comment = #{tableComment}, + sub_table_name = #{subTableName}, + sub_table_fk_name = #{subTableFkName}, + class_name = #{className}, + function_author = #{functionAuthor}, + gen_type = #{genType}, + gen_path = #{genPath}, + tpl_category = #{tplCategory}, + tpl_web_type = #{tplWebType}, + package_name = #{packageName}, + module_name = #{moduleName}, + business_name = #{businessName}, + function_name = #{functionName}, + options = #{options}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where table_id = #{tableId} + + + + delete from gen_table where table_id in + + #{tableId} + + + + \ No newline at end of file diff --git a/storm-modules/storm-gen/src/main/resources/vm/java/controller.java.vm b/storm-modules/storm-gen/src/main/resources/vm/java/controller.java.vm new file mode 100644 index 0000000..3b71c30 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/java/controller.java.vm @@ -0,0 +1,130 @@ +package ${packageName}.controller; + +import java.util.List; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; + +import com.storm.common.core.domain.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.utils.poi.ExcelUtil; +#if($table.crud || $table.sub) +import com.storm.common.core.web.page.TableDataInfo; +#elseif($table.tree) +#end + +/** + * ${functionName}Controller + * + * @author ${author} + * @date ${datetime} + */ +@RestController +@RequestMapping("/${businessName}") +@Tag(name = "${functionName}相关接口") +public class ${ClassName}Controller extends BaseController +{ + @Autowired + private I${ClassName}Service ${className}Service; + + /** + * 查询${functionName}列表 + */ + @RequiresPermissions("${permissionPrefix}:list") + @GetMapping("/list") + @Operation(summary = "查询${functionName}列表") +#if($table.crud || $table.sub) + public TableDataInfo> list(@Parameter(description = "${functionName}查询条件") ${ClassName} ${className}) + { + startPage(); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); + } +#elseif($table.tree) + public AjaxResult list(${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return success(list); + } +#end + + /** + * 导出${functionName}列表 + */ + @RequiresPermissions("${permissionPrefix}:export") + @Log(title = "${functionName}", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @Operation(summary = "导出${functionName}列表") + public void export(HttpServletResponse response, @Parameter(description = "${functionName}查询条件") ${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.exportExcel(response, list, "${functionName}数据"); + } + + /** + * 获取${functionName}详细信息 + */ + @RequiresPermissions("${permissionPrefix}:query") + @GetMapping(value = "/{${pkColumn.javaField}}") + @Operation(summary = "获取${functionName}详细信息") + public R<${ClassName}> getInfo(@Parameter(description = "${functionName}ID", required = true) + @PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) + { + ##return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + return R.ok(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + */ + @RequiresPermissions("${permissionPrefix}:add") + @Log(title = "${functionName}", businessType = BusinessType.INSERT) + @PostMapping + @Operation(summary = "新增${functionName}") + public AjaxResult add(@Parameter(description = "${functionName}实体", required = true) @RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.insert${ClassName}(${className})); + } + + /** + * 修改${functionName} + */ + @RequiresPermissions("${permissionPrefix}:edit") + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) + @PutMapping + @Operation(summary = "修改${functionName}") + public AjaxResult edit(@Parameter(description = "${functionName}实体", required = true) @RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.update${ClassName}(${className})); + } + + /** + * 删除${functionName} + */ + @RequiresPermissions("${permissionPrefix}:remove") + @Log(title = "${functionName}", businessType = BusinessType.DELETE) + @DeleteMapping("/{${pkColumn.javaField}s}") + @Operation(summary = "删除${functionName}") + public AjaxResult remove(@Parameter(description = "${functionName}ID数组", required = true) @PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s)); + } +} diff --git a/storm-modules/storm-gen/src/main/resources/vm/java/domain.java.vm b/storm-modules/storm-gen/src/main/resources/vm/java/domain.java.vm new file mode 100644 index 0000000..a6cd484 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/java/domain.java.vm @@ -0,0 +1,68 @@ +package ${packageName}.domain; + +#foreach ($import in $importList) +import ${import}; +#end +import io.swagger.v3.oas.annotations.media.Schema; +##import org.apache.commons.lang3.builder.ToStringBuilder; +##import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.annotation.Excel; +#if($table.crud || $table.sub) +import com.storm.common.core.web.domain.BaseEntity; +#elseif($table.tree) +import com.storm.common.core.web.domain.TreeEntity; +#end +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +/** + * ${functionName}对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +#if($table.crud || $table.sub) +#set($Entity="BaseEntity") +#elseif($table.tree) +#set($Entity="TreeEntity") +#end +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "${functionName}实体") +public class ${ClassName} extends ${Entity} +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date' || $column.javaType == 'LocalDateTime') + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + @Schema(description = "${column.columnComment}") + private $column.javaType $column.javaField; + +#end +#end +#if($table.sub) + /** $table.subTable.functionName信息 */ + private List<${subClassName}> ${subclassName}List; + +#end + +} diff --git a/storm-modules/storm-gen/src/main/resources/vm/java/mapper.java.vm b/storm-modules/storm-gen/src/main/resources/vm/java/mapper.java.vm new file mode 100644 index 0000000..5db0bad --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/java/mapper.java.vm @@ -0,0 +1,94 @@ +package ${packageName}.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import ${packageName}.domain.${ClassName}; +#if($table.sub) +import ${packageName}.domain.${subClassName}; +#end + +/** + * ${functionName}Mapper接口 + * + * @author ${author} + * @date ${datetime} + */ +@Mapper +public interface ${ClassName}Mapper extends BaseMapper<${ClassName}> +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 删除${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); +#if($table.sub) + + /** + * 批量删除${subTable.functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 批量新增${subTable.functionName} + * + * @param ${subclassName}List ${subTable.functionName}列表 + * @return 结果 + */ + public int batch${subClassName}(List<${subClassName}> ${subclassName}List); + + + /** + * 通过${functionName}主键删除${subTable.functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField}); +#end +} diff --git a/storm-modules/storm-gen/src/main/resources/vm/java/service.java.vm b/storm-modules/storm-gen/src/main/resources/vm/java/service.java.vm new file mode 100644 index 0000000..f869e39 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/java/service.java.vm @@ -0,0 +1,63 @@ +package ${packageName}.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName}Service接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface I${ClassName}Service extends IService<${ClassName}> +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); +} diff --git a/storm-modules/storm-gen/src/main/resources/vm/java/serviceImpl.java.vm b/storm-modules/storm-gen/src/main/resources/vm/java/serviceImpl.java.vm new file mode 100644 index 0000000..9bc34b8 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/java/serviceImpl.java.vm @@ -0,0 +1,166 @@ +package ${packageName}.service.impl; + +import java.util.List; +#foreach ($column in $columns) +#if($column.javaField == 'createTime' || $column.javaField == 'updateTime') +import com.storm.common.core.utils.DateUtils; +#break +#end +#end +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +#if($table.sub) +import java.util.ArrayList; +import com.storm.common.core.utils.StringUtils; +import org.springframework.transaction.annotation.Transactional; +import ${packageName}.domain.${subClassName}; +#end +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.Arrays; + +/** + * ${functionName}Service业务层处理 + * + * @author ${author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl extends ServiceImpl<${ClassName}Mapper, ${ClassName}> implements I${ClassName}Service +{ + @Autowired + private ${ClassName}Mapper ${className}Mapper; + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + @Override + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + ##return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + return getById(${pkColumn.javaField}); + } + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName} + */ + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) + { + return ${className}Mapper.select${ClassName}List(${className}); + } + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int insert${ClassName}(${ClassName} ${className}) + { +#if($table.sub) + int rows = ${className}Mapper.insert${ClassName}(${className}); + insert${subClassName}(${className}); + return rows; +#else + ##return ${className}Mapper.insert${ClassName}(${className}); + return save(${className}) ? 1 : 0; +#end + } + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int update${ClassName}(${ClassName} ${className}) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}()); + insert${subClassName}(${className}); +#end + ##return ${className}Mapper.update${ClassName}(${className}); + return updateById(${className}) ? 1 : 0; + } + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s); +#end + ##return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s); + return removeByIds(Arrays.asList(${pkColumn.javaField}s)) ? 1 : 0; + } + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField}); +#end + ##return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + return removeById(${pkColumn.javaField}) ? 1 : 0; + } +#if($table.sub) + + /** + * 新增${subTable.functionName}信息 + * + * @param ${className} ${functionName}对象 + */ + public void insert${subClassName}(${ClassName} ${className}) + { + List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List(); + ${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}(); + if (StringUtils.isNotNull(${subclassName}List)) + { + List<${subClassName}> list = new ArrayList<${subClassName}>(); + for (${subClassName} ${subclassName} : ${subclassName}List) + { + ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField}); + list.add(${subclassName}); + } + if (list.size() > 0) + { + ${className}Mapper.batch${subClassName}(list); + } + } + } +#end +} diff --git a/storm-modules/storm-gen/src/main/resources/vm/java/sub-domain.java.vm b/storm-modules/storm-gen/src/main/resources/vm/java/sub-domain.java.vm new file mode 100644 index 0000000..3824053 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/java/sub-domain.java.vm @@ -0,0 +1,76 @@ +package ${packageName}.domain; + +#foreach ($import in $subImportList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @author ${author} + * @date ${datetime} + */ +public class ${subClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } +#end +#end + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $subTable.columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end + .toString(); + } +} diff --git a/storm-modules/storm-gen/src/main/resources/vm/js/api.js.vm b/storm-modules/storm-gen/src/main/resources/vm/js/api.js.vm new file mode 100644 index 0000000..9295524 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/js/api.js.vm @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} diff --git a/storm-modules/storm-gen/src/main/resources/vm/sql/sql.vm b/storm-modules/storm-gen/src/main/resources/vm/sql/sql.vm new file mode 100644 index 0000000..0575583 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/sql/sql.vm @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); \ No newline at end of file diff --git a/storm-modules/storm-gen/src/main/resources/vm/vue/index-tree.vue.vm b/storm-modules/storm-gen/src/main/resources/vm/vue/index-tree.vue.vm new file mode 100644 index 0000000..4e35fc9 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/vue/index-tree.vue.vm @@ -0,0 +1,505 @@ + + + diff --git a/storm-modules/storm-gen/src/main/resources/vm/vue/index.vue.vm b/storm-modules/storm-gen/src/main/resources/vm/vue/index.vue.vm new file mode 100644 index 0000000..04ebe16 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/vue/index.vue.vm @@ -0,0 +1,602 @@ + + + diff --git a/storm-modules/storm-gen/src/main/resources/vm/vue/v3/index-tree.vue.vm b/storm-modules/storm-gen/src/main/resources/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 0000000..765a5e3 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,474 @@ + + + diff --git a/storm-modules/storm-gen/src/main/resources/vm/vue/v3/index.vue.vm b/storm-modules/storm-gen/src/main/resources/vm/vue/v3/index.vue.vm new file mode 100644 index 0000000..936b465 --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/vue/v3/index.vue.vm @@ -0,0 +1,590 @@ + + + diff --git a/storm-modules/storm-gen/src/main/resources/vm/xml/mapper.xml.vm b/storm-modules/storm-gen/src/main/resources/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..456755b --- /dev/null +++ b/storm-modules/storm-gen/src/main/resources/vm/xml/mapper.xml.vm @@ -0,0 +1,140 @@ + + + + + +#foreach ($column in $columns) + +#end + +#if($table.sub) + + + + + + +#foreach ($column in $subTable.columns) + +#end + +#end + + + select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName} + + + + + +#if($table.sub) + + +#end + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, +#end +#end + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + +#if($table.sub) + + + delete from ${subTableName} where ${subTableFkName} in + + #{${subTableFkclassName}} + + + + + delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}} + + + + insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values + + (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end) + + +#end + \ No newline at end of file diff --git a/storm-modules/storm-job/pom.xml b/storm-modules/storm-job/pom.xml new file mode 100644 index 0000000..7802cd2 --- /dev/null +++ b/storm-modules/storm-job/pom.xml @@ -0,0 +1,98 @@ + + + + com.storm + storm-modules + 3.6.6 + + 4.0.0 + + storm-modules-job + + + storm-modules-job定时任务 + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.quartz-scheduler + quartz + + + com.mchange + c3p0 + + + + + + + com.mysql + mysql-connector-j + + + + + com.storm + storm-common-log + + + + + com.storm + storm-common-swagger + + + + + org.projectlombok + lombok + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/JobApplication.java b/storm-modules/storm-job/src/main/java/com/storm/job/JobApplication.java new file mode 100644 index 0000000..99d45cf --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/JobApplication.java @@ -0,0 +1,46 @@ +package com.storm.job; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import com.storm.common.security.annotation.EnableCustomConfig; +import com.storm.common.security.annotation.EnableRyFeignClients; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.env.Environment; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 定时任务 + * + * @author ruoyi + */ +@Slf4j +@EnableCustomConfig +@EnableRyFeignClients +@SpringBootApplication +public class JobApplication +{ + public static void main(String[] args) throws UnknownHostException { + SpringApplication app = new SpringApplicationBuilder(JobApplication.class).build(args); + Environment env = app.run(args).getEnvironment(); + String protocol = "http"; + if (env.getProperty("server.ssl.key-store") != null) { + protocol = "https"; + } + log.info("--/\n---------------------------------------------------------------------------------------\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\t{}://localhost:{}\n\t" + + "External: \t{}://{}:{}\n\t" + + "Profile(s): \t{}" + + "\n---------------------------------------------------------------------------------------", + env.getProperty("spring.application.name"), + protocol, + env.getProperty("server.port"), + protocol, + InetAddress.getLocalHost().getHostAddress(), + env.getProperty("server.port"), + env.getActiveProfiles()); + } +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/config/ScheduleConfig.java b/storm-modules/storm-job/src/main/java/com/storm/job/config/ScheduleConfig.java new file mode 100644 index 0000000..1a772d0 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/config/ScheduleConfig.java @@ -0,0 +1,57 @@ +//package com.storm.job.config; +// +//import java.util.Properties; +//import javax.sql.DataSource; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.scheduling.quartz.SchedulerFactoryBean; +// +///** +// * 定时任务配置(单机部署建议删除此类和qrtz数据库表,默认走内存会最高效) +// * +// * @author ruoyi +// */ +//@Configuration +//public class ScheduleConfig +//{ +// @Bean +// public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) +// { +// SchedulerFactoryBean factory = new SchedulerFactoryBean(); +// factory.setDataSource(dataSource); +// +// // quartz参数 +// Properties prop = new Properties(); +// prop.put("org.quartz.scheduler.instanceName", "RuoyiScheduler"); +// prop.put("org.quartz.scheduler.instanceId", "AUTO"); +// // 线程池配置 +// prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); +// prop.put("org.quartz.threadPool.threadCount", "20"); +// prop.put("org.quartz.threadPool.threadPriority", "5"); +// // JobStore配置 +// prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); +// // 集群配置 +// prop.put("org.quartz.jobStore.isClustered", "true"); +// prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); +// prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "10"); +// prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); +// +// // sqlserver 启用 +// // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?"); +// prop.put("org.quartz.jobStore.misfireThreshold", "12000"); +// prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); +// factory.setQuartzProperties(prop); +// +// factory.setSchedulerName("RuoyiScheduler"); +// // 延时启动 +// factory.setStartupDelay(1); +// factory.setApplicationContextSchedulerContextKey("applicationContextKey"); +// // 可选,QuartzScheduler +// // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 +// factory.setOverwriteExistingJobs(true); +// // 设置自动启动,默认为true +// factory.setAutoStartup(true); +// +// return factory; +// } +//} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/controller/SysJobController.java b/storm-modules/storm-job/src/main/java/com/storm/job/controller/SysJobController.java new file mode 100644 index 0000000..c624194 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/controller/SysJobController.java @@ -0,0 +1,186 @@ +package com.storm.job.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.constant.Constants; +import com.storm.common.core.exception.job.TaskException; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.job.domain.SysJob; +import com.storm.job.service.ISysJobService; +import com.storm.job.util.CronUtils; +import com.storm.job.util.ScheduleUtils; + +/** + * 调度任务信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/job") +public class SysJobController extends BaseController +{ + @Autowired + private ISysJobService jobService; + + /** + * 查询定时任务列表 + */ + @RequiresPermissions("monitor:job:list") + @GetMapping("/list") + public TableDataInfo list(SysJob sysJob) + { + startPage(); + List list = jobService.selectJobList(sysJob); + return getDataTable(list); + } + + /** + * 导出定时任务列表 + */ + @RequiresPermissions("monitor:job:export") + @Log(title = "定时任务", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJob sysJob) + { + List list = jobService.selectJobList(sysJob); + ExcelUtil util = new ExcelUtil(SysJob.class); + util.exportExcel(response, list, "定时任务"); + } + + /** + * 获取定时任务详细信息 + */ + @RequiresPermissions("monitor:job:query") + @GetMapping(value = "/{jobId}") + public AjaxResult getInfo(@PathVariable("jobId") Long jobId) + { + return success(jobService.selectJobById(jobId)); + } + + /** + * 新增定时任务 + */ + @RequiresPermissions("monitor:job:add") + @Log(title = "定时任务", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setCreateBy(SecurityUtils.getUsername()); + return toAjax(jobService.insertJob(job)); + } + + /** + * 修改定时任务 + */ + @RequiresPermissions("monitor:job:edit") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(jobService.updateJob(job)); + } + + /** + * 定时任务状态修改 + */ + @RequiresPermissions("monitor:job:changeStatus") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException + { + SysJob newJob = jobService.selectJobById(job.getJobId()); + newJob.setStatus(job.getStatus()); + return toAjax(jobService.changeStatus(newJob)); + } + + /** + * 定时任务立即执行一次 + */ + @RequiresPermissions("monitor:job:changeStatus") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/run") + public AjaxResult run(@RequestBody SysJob job) throws SchedulerException + { + boolean result = jobService.run(job); + return result ? success() : error("任务不存在或已过期!"); + } + + /** + * 删除定时任务 + */ + @RequiresPermissions("monitor:job:remove") + @Log(title = "定时任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobIds}") + public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException + { + jobService.deleteJobByIds(jobIds); + return success(); + } +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/controller/SysJobLogController.java b/storm-modules/storm-job/src/main/java/com/storm/job/controller/SysJobLogController.java new file mode 100644 index 0000000..d15f8fe --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/controller/SysJobLogController.java @@ -0,0 +1,91 @@ +package com.storm.job.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.job.domain.SysJobLog; +import com.storm.job.service.ISysJobLogService; + +/** + * 调度日志操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/job/log") +public class SysJobLogController extends BaseController +{ + @Autowired + private ISysJobLogService jobLogService; + + /** + * 查询定时任务调度日志列表 + */ + @RequiresPermissions("monitor:job:list") + @GetMapping("/list") + public TableDataInfo list(SysJobLog sysJobLog) + { + startPage(); + List list = jobLogService.selectJobLogList(sysJobLog); + return getDataTable(list); + } + + /** + * 导出定时任务调度日志列表 + */ + @RequiresPermissions("monitor:job:export") + @Log(title = "任务调度日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJobLog sysJobLog) + { + List list = jobLogService.selectJobLogList(sysJobLog); + ExcelUtil util = new ExcelUtil(SysJobLog.class); + util.exportExcel(response, list, "调度日志"); + } + + /** + * 根据调度编号获取详细信息 + */ + @RequiresPermissions("monitor:job:query") + @GetMapping(value = "/{jobLogId}") + public AjaxResult getInfo(@PathVariable Long jobLogId) + { + return success(jobLogService.selectJobLogById(jobLogId)); + } + + /** + * 删除定时任务调度日志 + */ + @RequiresPermissions("monitor:job:remove") + @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobLogIds}") + public AjaxResult remove(@PathVariable Long[] jobLogIds) + { + return toAjax(jobLogService.deleteJobLogByIds(jobLogIds)); + } + + /** + * 清空定时任务调度日志 + */ + @RequiresPermissions("monitor:job:remove") + @Log(title = "调度日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + jobLogService.cleanJobLog(); + return success(); + } +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/domain/SysJob.java b/storm-modules/storm-job/src/main/java/com/storm/job/domain/SysJob.java new file mode 100644 index 0000000..9cf78d8 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/domain/SysJob.java @@ -0,0 +1,171 @@ +package com.storm.job.domain; + +import java.util.Date; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.annotation.Excel.ColumnType; +import com.storm.common.core.constant.ScheduleConstants; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.web.domain.BaseEntity; +import com.storm.job.util.CronUtils; + +/** + * 定时任务调度表 sys_job + * + * @author ruoyi + */ +public class SysJob extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 任务ID */ + @Excel(name = "任务序号", cellType = ColumnType.NUMERIC) + private Long jobId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** cron执行表达式 */ + @Excel(name = "执行表达式 ") + private String cronExpression; + + /** cron计划策略 */ + @Excel(name = "计划策略 ", readConverterExp = "0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行") + private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT; + + /** 是否并发执行(0允许 1禁止) */ + @Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止") + private String concurrent; + + /** 任务状态(0正常 1暂停) */ + @Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停") + private String status; + + public Long getJobId() + { + return jobId; + } + + public void setJobId(Long jobId) + { + this.jobId = jobId; + } + + @NotBlank(message = "任务名称不能为空") + @Size(min = 0, max = 64, message = "任务名称不能超过64个字符") + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + @NotBlank(message = "调用目标字符串不能为空") + @Size(min = 0, max = 500, message = "调用目标字符串长度不能超过500个字符") + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + @NotBlank(message = "Cron执行表达式不能为空") + @Size(min = 0, max = 255, message = "Cron执行表达式不能超过255个字符") + public String getCronExpression() + { + return cronExpression; + } + + public void setCronExpression(String cronExpression) + { + this.cronExpression = cronExpression; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + public Date getNextValidTime() + { + if (StringUtils.isNotEmpty(cronExpression)) + { + return CronUtils.getNextExecution(cronExpression); + } + return null; + } + + public String getMisfirePolicy() + { + return misfirePolicy; + } + + public void setMisfirePolicy(String misfirePolicy) + { + this.misfirePolicy = misfirePolicy; + } + + public String getConcurrent() + { + return concurrent; + } + + public void setConcurrent(String concurrent) + { + this.concurrent = concurrent; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobId", getJobId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("cronExpression", getCronExpression()) + .append("nextValidTime", getNextValidTime()) + .append("misfirePolicy", getMisfirePolicy()) + .append("concurrent", getConcurrent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} \ No newline at end of file diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/domain/SysJobLog.java b/storm-modules/storm-job/src/main/java/com/storm/job/domain/SysJobLog.java new file mode 100644 index 0000000..e6dd251 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/domain/SysJobLog.java @@ -0,0 +1,155 @@ +package com.storm.job.domain; + +import java.util.Date; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 定时任务调度日志表 sys_job_log + * + * @author ruoyi + */ +public class SysJobLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "日志序号") + private Long jobLogId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** 日志信息 */ + @Excel(name = "日志信息") + private String jobMessage; + + /** 执行状态(0正常 1失败) */ + @Excel(name = "执行状态", readConverterExp = "0=正常,1=失败") + private String status; + + /** 异常信息 */ + @Excel(name = "异常信息") + private String exceptionInfo; + + /** 开始时间 */ + private Date startTime; + + /** 停止时间 */ + private Date stopTime; + + public Long getJobLogId() + { + return jobLogId; + } + + public void setJobLogId(Long jobLogId) + { + this.jobLogId = jobLogId; + } + + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + public String getJobMessage() + { + return jobMessage; + } + + public void setJobMessage(String jobMessage) + { + this.jobMessage = jobMessage; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getExceptionInfo() + { + return exceptionInfo; + } + + public void setExceptionInfo(String exceptionInfo) + { + this.exceptionInfo = exceptionInfo; + } + + public Date getStartTime() + { + return startTime; + } + + public void setStartTime(Date startTime) + { + this.startTime = startTime; + } + + public Date getStopTime() + { + return stopTime; + } + + public void setStopTime(Date stopTime) + { + this.stopTime = stopTime; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobLogId", getJobLogId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("jobMessage", getJobMessage()) + .append("status", getStatus()) + .append("exceptionInfo", getExceptionInfo()) + .append("startTime", getStartTime()) + .append("stopTime", getStopTime()) + .toString(); + } +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/mapper/SysJobLogMapper.java b/storm-modules/storm-job/src/main/java/com/storm/job/mapper/SysJobLogMapper.java new file mode 100644 index 0000000..9520eb5 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/mapper/SysJobLogMapper.java @@ -0,0 +1,64 @@ +package com.storm.job.mapper; + +import java.util.List; +import com.storm.job.domain.SysJobLog; + +/** + * 调度任务日志信息 数据层 + * + * @author ruoyi + */ +public interface SysJobLogMapper +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 查询所有调度任务日志 + * + * @return 调度任务日志列表 + */ + public List selectJobLogAll(); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + * @return 结果 + */ + public int insertJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/mapper/SysJobMapper.java b/storm-modules/storm-job/src/main/java/com/storm/job/mapper/SysJobMapper.java new file mode 100644 index 0000000..1db8fc2 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/mapper/SysJobMapper.java @@ -0,0 +1,67 @@ +package com.storm.job.mapper; + +import java.util.List; +import com.storm.job.domain.SysJob; + +/** + * 调度任务信息 数据层 + * + * @author ruoyi + */ +public interface SysJobMapper +{ + /** + * 查询调度任务日志集合 + * + * @param job 调度信息 + * @return 操作日志集合 + */ + public List selectJobList(SysJob job); + + /** + * 查询所有调度任务 + * + * @return 调度任务列表 + */ + public List selectJobAll(); + + /** + * 通过调度ID查询调度任务信息 + * + * @param jobId 调度ID + * @return 角色对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 通过调度ID删除调度任务信息 + * + * @param jobId 调度ID + * @return 结果 + */ + public int deleteJobById(Long jobId); + + /** + * 批量删除调度任务信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteJobByIds(Long[] ids); + + /** + * 修改调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int updateJob(SysJob job); + + /** + * 新增调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int insertJob(SysJob job); +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/service/ISysJobLogService.java b/storm-modules/storm-job/src/main/java/com/storm/job/service/ISysJobLogService.java new file mode 100644 index 0000000..8789784 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/service/ISysJobLogService.java @@ -0,0 +1,56 @@ +package com.storm.job.service; + +import java.util.List; +import com.storm.job.domain.SysJobLog; + +/** + * 定时任务调度日志信息信息 服务层 + * + * @author ruoyi + */ +public interface ISysJobLogService +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + public void addJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的日志ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/service/ISysJobService.java b/storm-modules/storm-job/src/main/java/com/storm/job/service/ISysJobService.java new file mode 100644 index 0000000..76e3d8f --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/service/ISysJobService.java @@ -0,0 +1,102 @@ +package com.storm.job.service; + +import java.util.List; +import org.quartz.SchedulerException; +import com.storm.common.core.exception.job.TaskException; +import com.storm.job.domain.SysJob; + +/** + * 定时任务调度信息信息 服务层 + * + * @author ruoyi + */ +public interface ISysJobService +{ + /** + * 获取quartz调度器的计划任务 + * + * @param job 调度信息 + * @return 调度任务集合 + */ + public List selectJobList(SysJob job); + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 暂停任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int pauseJob(SysJob job) throws SchedulerException; + + /** + * 恢复任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int resumeJob(SysJob job) throws SchedulerException; + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + * @return 结果 + */ + public int deleteJob(SysJob job) throws SchedulerException; + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + public void deleteJobByIds(Long[] jobIds) throws SchedulerException; + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + * @return 结果 + */ + public int changeStatus(SysJob job) throws SchedulerException; + + /** + * 立即运行任务 + * + * @param job 调度信息 + * @return 结果 + */ + public boolean run(SysJob job) throws SchedulerException; + + /** + * 新增任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int insertJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 更新任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int updateJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + public boolean checkCronExpressionIsValid(String cronExpression); +} \ No newline at end of file diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/service/SysJobLogServiceImpl.java b/storm-modules/storm-job/src/main/java/com/storm/job/service/SysJobLogServiceImpl.java new file mode 100644 index 0000000..09aca62 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/service/SysJobLogServiceImpl.java @@ -0,0 +1,86 @@ +package com.storm.job.service; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.job.domain.SysJobLog; +import com.storm.job.mapper.SysJobLogMapper; + +/** + * 定时任务调度日志信息 服务层 + * + * @author ruoyi + */ +@Service +public class SysJobLogServiceImpl implements ISysJobLogService +{ + @Autowired + private SysJobLogMapper jobLogMapper; + + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + @Override + public List selectJobLogList(SysJobLog jobLog) + { + return jobLogMapper.selectJobLogList(jobLog); + } + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + @Override + public SysJobLog selectJobLogById(Long jobLogId) + { + return jobLogMapper.selectJobLogById(jobLogId); + } + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + @Override + public void addJobLog(SysJobLog jobLog) + { + jobLogMapper.insertJobLog(jobLog); + } + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteJobLogByIds(Long[] logIds) + { + return jobLogMapper.deleteJobLogByIds(logIds); + } + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + */ + @Override + public int deleteJobLogById(Long jobId) + { + return jobLogMapper.deleteJobLogById(jobId); + } + + /** + * 清空任务日志 + */ + @Override + public void cleanJobLog() + { + jobLogMapper.cleanJobLog(); + } +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/service/SysJobServiceImpl.java b/storm-modules/storm-job/src/main/java/com/storm/job/service/SysJobServiceImpl.java new file mode 100644 index 0000000..726f8ad --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/service/SysJobServiceImpl.java @@ -0,0 +1,260 @@ +package com.storm.job.service; + +import java.util.List; +import javax.annotation.PostConstruct; +import org.quartz.JobDataMap; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.storm.common.core.constant.ScheduleConstants; +import com.storm.common.core.exception.job.TaskException; +import com.storm.job.domain.SysJob; +import com.storm.job.mapper.SysJobMapper; +import com.storm.job.util.CronUtils; +import com.storm.job.util.ScheduleUtils; + +/** + * 定时任务调度信息 服务层 + * + * @author ruoyi + */ +@Service +public class SysJobServiceImpl implements ISysJobService +{ + @Autowired + private Scheduler scheduler; + + @Autowired + private SysJobMapper jobMapper; + + /** + * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据) + */ + @PostConstruct + public void init() throws SchedulerException, TaskException + { + scheduler.clear(); + List jobList = jobMapper.selectJobAll(); + for (SysJob job : jobList) + { + ScheduleUtils.createScheduleJob(scheduler, job); + } + } + + /** + * 获取quartz调度器的计划任务列表 + * + * @param job 调度信息 + * @return + */ + @Override + public List selectJobList(SysJob job) + { + return jobMapper.selectJobList(job); + } + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + @Override + public SysJob selectJobById(Long jobId) + { + return jobMapper.selectJobById(jobId); + } + + /** + * 暂停任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int pauseJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 恢复任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int resumeJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.NORMAL.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + int rows = jobMapper.deleteJobById(jobId); + if (rows > 0) + { + scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJobByIds(Long[] jobIds) throws SchedulerException + { + for (Long jobId : jobIds) + { + SysJob job = jobMapper.selectJobById(jobId); + deleteJob(job); + } + } + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int changeStatus(SysJob job) throws SchedulerException + { + int rows = 0; + String status = job.getStatus(); + if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) + { + rows = resumeJob(job); + } + else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) + { + rows = pauseJob(job); + } + return rows; + } + + /** + * 立即运行任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean run(SysJob job) throws SchedulerException + { + boolean result = false; + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + SysJob properties = selectJobById(job.getJobId()); + // 参数 + JobDataMap dataMap = new JobDataMap(); + dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties); + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) + { + result = true; + scheduler.triggerJob(jobKey, dataMap); + } + return result; + } + + /** + * 新增任务 + * + * @param job 调度信息 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertJob(SysJob job) throws SchedulerException, TaskException + { + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.insertJob(job); + if (rows > 0) + { + ScheduleUtils.createScheduleJob(scheduler, job); + } + return rows; + } + + /** + * 更新任务的时间表达式 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateJob(SysJob job) throws SchedulerException, TaskException + { + SysJob properties = selectJobById(job.getJobId()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + updateSchedulerJob(job, properties.getJobGroup()); + } + return rows; + } + + /** + * 更新任务 + * + * @param job 任务对象 + * @param jobGroup 任务组名 + */ + public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException, TaskException + { + Long jobId = job.getJobId(); + // 判断是否存在 + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(jobKey); + } + ScheduleUtils.createScheduleJob(scheduler, job); + } + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + @Override + public boolean checkCronExpressionIsValid(String cronExpression) + { + return CronUtils.isValid(cronExpression); + } +} \ No newline at end of file diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/task/RyTask.java b/storm-modules/storm-job/src/main/java/com/storm/job/task/RyTask.java new file mode 100644 index 0000000..3d12510 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/task/RyTask.java @@ -0,0 +1,28 @@ +package com.storm.job.task; + +import org.springframework.stereotype.Component; +import com.storm.common.core.utils.StringUtils; + +/** + * 定时任务调度测试 + * + * @author ruoyi + */ +@Component("ryTask") +public class RyTask +{ + public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) + { + System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); + } + + public void ryParams(String params) + { + System.out.println("执行有参方法:" + params); + } + + public void ryNoParams() + { + System.out.println("执行无参方法"); + } +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/util/AbstractQuartzJob.java b/storm-modules/storm-job/src/main/java/com/storm/job/util/AbstractQuartzJob.java new file mode 100644 index 0000000..375bfa8 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/util/AbstractQuartzJob.java @@ -0,0 +1,105 @@ +package com.storm.job.util; + +import java.util.Date; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.storm.common.core.constant.ScheduleConstants; +import com.storm.common.core.utils.ExceptionUtil; +import com.storm.common.core.utils.SpringUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.bean.BeanUtils; +import com.storm.job.domain.SysJob; +import com.storm.job.domain.SysJobLog; +import com.storm.job.service.ISysJobLogService; + +/** + * 抽象quartz调用 + * + * @author ruoyi + */ +public abstract class AbstractQuartzJob implements Job +{ + private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class); + + /** + * 线程本地变量 + */ + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + @Override + public void execute(JobExecutionContext context) + { + SysJob sysJob = new SysJob(); + BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); + try + { + before(context, sysJob); + if (sysJob != null) + { + doExecute(context, sysJob); + } + after(context, sysJob, null); + } + catch (Exception e) + { + log.error("任务执行异常 - :", e); + after(context, sysJob, e); + } + } + + /** + * 执行前 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void before(JobExecutionContext context, SysJob sysJob) + { + threadLocal.set(new Date()); + } + + /** + * 执行后 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void after(JobExecutionContext context, SysJob sysJob, Exception e) + { + Date startTime = threadLocal.get(); + threadLocal.remove(); + + final SysJobLog sysJobLog = new SysJobLog(); + sysJobLog.setJobName(sysJob.getJobName()); + sysJobLog.setJobGroup(sysJob.getJobGroup()); + sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); + sysJobLog.setStartTime(startTime); + sysJobLog.setStopTime(new Date()); + long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + if (e != null) + { + sysJobLog.setStatus("1"); + String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); + sysJobLog.setExceptionInfo(errorMsg); + } + else + { + sysJobLog.setStatus("0"); + } + + // 写入数据库当中 + SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); + } + + /** + * 执行方法,由子类重载 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + * @throws Exception 执行过程中的异常 + */ + protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception; +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/util/CronUtils.java b/storm-modules/storm-job/src/main/java/com/storm/job/util/CronUtils.java new file mode 100644 index 0000000..f62fc67 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/util/CronUtils.java @@ -0,0 +1,63 @@ +package com.storm.job.util; + +import java.text.ParseException; +import java.util.Date; +import org.quartz.CronExpression; + +/** + * cron表达式工具类 + * + * @author ruoyi + * + */ +public class CronUtils +{ + /** + * 返回一个布尔值代表一个给定的Cron表达式的有效性 + * + * @param cronExpression Cron表达式 + * @return boolean 表达式是否有效 + */ + public static boolean isValid(String cronExpression) + { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 返回一个字符串值,表示该消息无效Cron表达式给出有效性 + * + * @param cronExpression Cron表达式 + * @return String 无效时返回表达式错误描述,如果有效返回null + */ + public static String getInvalidMessage(String cronExpression) + { + try + { + new CronExpression(cronExpression); + return null; + } + catch (ParseException pe) + { + return pe.getMessage(); + } + } + + /** + * 返回下一个执行时间根据给定的Cron表达式 + * + * @param cronExpression Cron表达式 + * @return Date 下次Cron表达式执行时间 + */ + public static Date getNextExecution(String cronExpression) + { + try + { + CronExpression cron = new CronExpression(cronExpression); + return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis())); + } + catch (ParseException e) + { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/util/JobInvokeUtil.java b/storm-modules/storm-job/src/main/java/com/storm/job/util/JobInvokeUtil.java new file mode 100644 index 0000000..56803e2 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/util/JobInvokeUtil.java @@ -0,0 +1,182 @@ +package com.storm.job.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; +import com.storm.common.core.utils.SpringUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.job.domain.SysJob; + +/** + * 任务执行工具 + * + * @author ruoyi + */ +public class JobInvokeUtil +{ + /** + * 执行方法 + * + * @param sysJob 系统任务 + */ + public static void invokeMethod(SysJob sysJob) throws Exception + { + String invokeTarget = sysJob.getInvokeTarget(); + String beanName = getBeanName(invokeTarget); + String methodName = getMethodName(invokeTarget); + List methodParams = getMethodParams(invokeTarget); + + if (!isValidClassName(beanName)) + { + Object bean = SpringUtils.getBean(beanName); + invokeMethod(bean, methodName, methodParams); + } + else + { + Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance(); + invokeMethod(bean, methodName, methodParams); + } + } + + /** + * 调用任务方法 + * + * @param bean 目标对象 + * @param methodName 方法名称 + * @param methodParams 方法参数 + */ + private static void invokeMethod(Object bean, String methodName, List methodParams) + throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException + { + if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) + { + Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams)); + method.invoke(bean, getMethodParamsValue(methodParams)); + } + else + { + Method method = bean.getClass().getMethod(methodName); + method.invoke(bean); + } + } + + /** + * 校验是否为为class包名 + * + * @param invokeTarget 名称 + * @return true是 false否 + */ + public static boolean isValidClassName(String invokeTarget) + { + return StringUtils.countMatches(invokeTarget, ".") > 1; + } + + /** + * 获取bean名称 + * + * @param invokeTarget 目标字符串 + * @return bean名称 + */ + public static String getBeanName(String invokeTarget) + { + String beanName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringBeforeLast(beanName, "."); + } + + /** + * 获取bean方法 + * + * @param invokeTarget 目标字符串 + * @return method方法 + */ + public static String getMethodName(String invokeTarget) + { + String methodName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringAfterLast(methodName, "."); + } + + /** + * 获取method方法参数相关列表 + * + * @param invokeTarget 目标字符串 + * @return method方法相关参数列表 + */ + public static List getMethodParams(String invokeTarget) + { + String methodStr = StringUtils.substringBetweenLast(invokeTarget, "(", ")"); + if (StringUtils.isEmpty(methodStr)) + { + return null; + } + String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); + List classs = new LinkedList<>(); + for (int i = 0; i < methodParams.length; i++) + { + String str = StringUtils.trimToEmpty(methodParams[i]); + // String字符串类型,以'或"开头 + if (StringUtils.startsWithAny(str, "'", "\"")) + { + classs.add(new Object[] { StringUtils.substring(str, 1, str.length() - 1), String.class }); + } + // boolean布尔类型,等于true或者false + else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) + { + classs.add(new Object[] { Boolean.valueOf(str), Boolean.class }); + } + // long长整形,以L结尾 + else if (StringUtils.endsWith(str, "L")) + { + classs.add(new Object[] { Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class }); + } + // double浮点类型,以D结尾 + else if (StringUtils.endsWith(str, "D")) + { + classs.add(new Object[] { Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class }); + } + // 其他类型归类为整形 + else + { + classs.add(new Object[] { Integer.valueOf(str), Integer.class }); + } + } + return classs; + } + + /** + * 获取参数类型 + * + * @param methodParams 参数相关列表 + * @return 参数类型列表 + */ + public static Class[] getMethodParamsType(List methodParams) + { + Class[] classs = new Class[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Class) os[1]; + index++; + } + return classs; + } + + /** + * 获取参数值 + * + * @param methodParams 参数相关列表 + * @return 参数值列表 + */ + public static Object[] getMethodParamsValue(List methodParams) + { + Object[] classs = new Object[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Object) os[0]; + index++; + } + return classs; + } +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/util/QuartzDisallowConcurrentExecution.java b/storm-modules/storm-job/src/main/java/com/storm/job/util/QuartzDisallowConcurrentExecution.java new file mode 100644 index 0000000..6016c0f --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/util/QuartzDisallowConcurrentExecution.java @@ -0,0 +1,22 @@ +package com.storm.job.util; + +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; + +import com.storm.job.domain.SysJob; + +/** + * 定时任务处理(禁止并发执行) + * + * @author ruoyi + * + */ +@DisallowConcurrentExecution +public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/util/QuartzJobExecution.java b/storm-modules/storm-job/src/main/java/com/storm/job/util/QuartzJobExecution.java new file mode 100644 index 0000000..010bd38 --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/util/QuartzJobExecution.java @@ -0,0 +1,20 @@ +package com.storm.job.util; + +import org.quartz.JobExecutionContext; + +import com.storm.job.domain.SysJob; + +/** + * 定时任务处理(允许并发执行) + * + * @author ruoyi + * + */ +public class QuartzJobExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/storm-modules/storm-job/src/main/java/com/storm/job/util/ScheduleUtils.java b/storm-modules/storm-job/src/main/java/com/storm/job/util/ScheduleUtils.java new file mode 100644 index 0000000..743c49c --- /dev/null +++ b/storm-modules/storm-job/src/main/java/com/storm/job/util/ScheduleUtils.java @@ -0,0 +1,141 @@ +package com.storm.job.util; + +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import com.storm.common.core.constant.Constants; +import com.storm.common.core.constant.ScheduleConstants; +import com.storm.common.core.exception.job.TaskException; +import com.storm.common.core.exception.job.TaskException.Code; +import com.storm.common.core.utils.SpringUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.job.domain.SysJob; + +/** + * 定时任务工具类 + * + * @author ruoyi + * + */ +public class ScheduleUtils +{ + /** + * 得到quartz任务类 + * + * @param sysJob 执行计划 + * @return 具体执行任务类 + */ + private static Class getQuartzJobClass(SysJob sysJob) + { + boolean isConcurrent = "0".equals(sysJob.getConcurrent()); + return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class; + } + + /** + * 构建任务触发对象 + */ + public static TriggerKey getTriggerKey(Long jobId, String jobGroup) + { + return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 构建任务键对象 + */ + public static JobKey getJobKey(Long jobId, String jobGroup) + { + return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 创建定时任务 + */ + public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException + { + Class jobClass = getQuartzJobClass(job); + // 构建job信息 + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build(); + + // 表达式调度构建器 + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()); + cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder); + + // 按新的cronExpression表达式构建一个新的trigger + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup)) + .withSchedule(cronScheduleBuilder).build(); + + // 放入参数,运行时的方法可以获取 + jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job); + + // 判断是否存在 + if (scheduler.checkExists(getJobKey(jobId, jobGroup))) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(getJobKey(jobId, jobGroup)); + } + + // 判断任务是否过期 + if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) + { + // 执行调度任务 + scheduler.scheduleJob(jobDetail, trigger); + } + + // 暂停任务 + if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + } + + /** + * 设置定时任务策略 + */ + public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb) + throws TaskException + { + switch (job.getMisfirePolicy()) + { + case ScheduleConstants.MISFIRE_DEFAULT: + return cb; + case ScheduleConstants.MISFIRE_IGNORE_MISFIRES: + return cb.withMisfireHandlingInstructionIgnoreMisfires(); + case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED: + return cb.withMisfireHandlingInstructionFireAndProceed(); + case ScheduleConstants.MISFIRE_DO_NOTHING: + return cb.withMisfireHandlingInstructionDoNothing(); + default: + throw new TaskException("The task misfire policy '" + job.getMisfirePolicy() + + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR); + } + } + + /** + * 检查包名是否为白名单配置 + * + * @param invokeTarget 目标字符串 + * @return 结果 + */ + public static boolean whiteList(String invokeTarget) + { + String packageName = StringUtils.substringBefore(invokeTarget, "("); + int count = StringUtils.countMatches(packageName, "."); + if (count > 1) + { + return StringUtils.startsWithAny(invokeTarget, Constants.JOB_WHITELIST_STR); + } + Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]); + String beanPackageName = obj.getClass().getPackage().getName(); + return StringUtils.startsWithAny(beanPackageName, Constants.JOB_WHITELIST_STR) + && !StringUtils.startsWithAny(beanPackageName, Constants.JOB_ERROR_STR); + } +} \ No newline at end of file diff --git a/storm-modules/storm-job/src/main/resources/banner.txt b/storm-modules/storm-job/src/main/resources/banner.txt new file mode 100644 index 0000000..0b9cd42 --- /dev/null +++ b/storm-modules/storm-job/src/main/resources/banner.txt @@ -0,0 +1,10 @@ +Spring Boot Version: ${spring-boot.version} +Spring Application Name: ${spring.application.name} + _ _ _ + (_) (_) | | + _ __ _ _ ___ _ _ _ ______ _ ___ | |__ +| '__|| | | | / _ \ | | | || ||______| | | / _ \ | '_ \ +| | | |_| || (_) || |_| || | | || (_) || |_) | +|_| \__,_| \___/ \__, ||_| | | \___/ |_.__/ + __/ | _/ | + |___/ |__/ \ No newline at end of file diff --git a/storm-modules/storm-job/src/main/resources/bootstrap.yml b/storm-modules/storm-job/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..61b7318 --- /dev/null +++ b/storm-modules/storm-job/src/main/resources/bootstrap.yml @@ -0,0 +1,44 @@ +# Tomcat +server: + port: 9203 + +# Spring +spring: + application: + # 应用名称 + name: storm-job + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: storm-nacos:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: storm-nacos:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} +# mybatis-plus配置 +mybatis-plus: + # 搜索指定包别名 + typeAliasesPackage: com.storm.job.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath:mapper/**/*Mapper.xml + # 全局配置 + global-config: + db-config: + id-type: auto #id生成策略为自增 + configuration: + map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名 diff --git a/storm-modules/storm-job/src/main/resources/bootstrap.yml01 b/storm-modules/storm-job/src/main/resources/bootstrap.yml01 new file mode 100644 index 0000000..a541de3 --- /dev/null +++ b/storm-modules/storm-job/src/main/resources/bootstrap.yml01 @@ -0,0 +1,44 @@ +# Tomcat +server: + port: 9203 + +# Spring +spring: + application: + # 应用名称 + name: storm-job + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} +# mybatis-plus配置 +mybatis-plus: + # 搜索指定包别名 + typeAliasesPackage: com.storm.job.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath:mapper/**/*Mapper.xml + # 全局配置 + global-config: + db-config: + id-type: auto #id生成策略为自增 + configuration: + map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名 \ No newline at end of file diff --git a/storm-modules/storm-job/src/main/resources/logback.xml b/storm-modules/storm-job/src/main/resources/logback.xml new file mode 100644 index 0000000..8d0c84e --- /dev/null +++ b/storm-modules/storm-job/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/storm-modules/storm-job/src/main/resources/mapper/job/SysJobLogMapper.xml b/storm-modules/storm-job/src/main/resources/mapper/job/SysJobLogMapper.xml new file mode 100644 index 0000000..80c85af --- /dev/null +++ b/storm-modules/storm-job/src/main/resources/mapper/job/SysJobLogMapper.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, create_time + from sys_job_log + + + + + + + + + + delete from sys_job_log where job_log_id = #{jobLogId} + + + + delete from sys_job_log where job_log_id in + + #{jobLogId} + + + + + truncate table sys_job_log + + + + insert into sys_job_log( + job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + create_time + )values( + #{jobLogId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{jobMessage}, + #{status}, + #{exceptionInfo}, + sysdate() + ) + + + \ No newline at end of file diff --git a/storm-modules/storm-job/src/main/resources/mapper/job/SysJobMapper.xml b/storm-modules/storm-job/src/main/resources/mapper/job/SysJobMapper.xml new file mode 100644 index 0000000..3fc8411 --- /dev/null +++ b/storm-modules/storm-job/src/main/resources/mapper/job/SysJobMapper.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark + from sys_job + + + + + + + + + + delete from sys_job where job_id = #{jobId} + + + + delete from sys_job where job_id in + + #{jobId} + + + + + update sys_job + + job_name = #{jobName}, + job_group = #{jobGroup}, + invoke_target = #{invokeTarget}, + cron_expression = #{cronExpression}, + misfire_policy = #{misfirePolicy}, + concurrent = #{concurrent}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where job_id = #{jobId} + + + + insert into sys_job( + job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + remark, + create_by, + create_time + )values( + #{jobId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{cronExpression}, + #{misfirePolicy}, + #{concurrent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/storm-modules/storm-system/pom.xml b/storm-modules/storm-system/pom.xml new file mode 100644 index 0000000..86dac57 --- /dev/null +++ b/storm-modules/storm-system/pom.xml @@ -0,0 +1,98 @@ + + + + com.storm + storm-modules + 3.6.6 + + 4.0.0 + + storm-modules-system + + + storm-modules-system系统模块 + + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + com.mysql + mysql-connector-j + + + + + com.storm + storm-common-datasource + + + + + com.storm + storm-common-datascope + + + + + com.storm + storm-common-log + + + + + com.storm + storm-common-swagger + + + + + org.projectlombok + lombok + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/SystemApplication.java b/storm-modules/storm-system/src/main/java/com/storm/system/SystemApplication.java new file mode 100644 index 0000000..3e0c4f1 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/SystemApplication.java @@ -0,0 +1,46 @@ +package com.storm.system; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import com.storm.common.security.annotation.EnableCustomConfig; +import com.storm.common.security.annotation.EnableRyFeignClients; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.env.Environment; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 系统模块 + * + * @author ruoyi + */ +@Slf4j +@EnableCustomConfig +@EnableRyFeignClients +@SpringBootApplication +public class SystemApplication +{ + public static void main(String[] args) throws UnknownHostException { + SpringApplication app = new SpringApplicationBuilder(SystemApplication.class).build(args); + Environment env = app.run(args).getEnvironment(); + String protocol = "http"; + if (env.getProperty("server.ssl.key-store") != null) { + protocol = "https"; + } + log.info("--/\n---------------------------------------------------------------------------------------\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\t{}://localhost:{}\n\t" + + "External: \t{}://{}:{}\n\t" + + "Profile(s): \t{}" + + "\n---------------------------------------------------------------------------------------", + env.getProperty("spring.application.name"), + protocol, + env.getProperty("server.port"), + protocol, + InetAddress.getLocalHost().getHostAddress(), + env.getProperty("server.port"), + env.getActiveProfiles()); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysConfigController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysConfigController.java new file mode 100644 index 0000000..8169bcd --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysConfigController.java @@ -0,0 +1,133 @@ +package com.storm.system.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.domain.SysConfig; +import com.storm.system.service.ISysConfigService; + +/** + * 参数配置 信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/config") +public class SysConfigController extends BaseController +{ + @Autowired + private ISysConfigService configService; + + /** + * 获取参数配置列表 + */ + @RequiresPermissions("system:config:list") + @GetMapping("/list") + public TableDataInfo list(SysConfig config) + { + startPage(); + List list = configService.selectConfigList(config); + return getDataTable(list); + } + + @Log(title = "参数管理", businessType = BusinessType.EXPORT) + @RequiresPermissions("system:config:export") + @PostMapping("/export") + public void export(HttpServletResponse response, SysConfig config) + { + List list = configService.selectConfigList(config); + ExcelUtil util = new ExcelUtil(SysConfig.class); + util.exportExcel(response, list, "参数数据"); + } + + /** + * 根据参数编号获取详细信息 + */ + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long configId) + { + return success(configService.selectConfigById(configId)); + } + + /** + * 根据参数键名查询参数值 + */ + @GetMapping(value = "/configKey/{configKey}") + public AjaxResult getConfigKey(@PathVariable String configKey) + { + return success(configService.selectConfigByKey(configKey)); + } + + /** + * 新增参数配置 + */ + @RequiresPermissions("system:config:add") + @Log(title = "参数管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysConfig config) + { + if (!configService.checkConfigKeyUnique(config)) + { + return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setCreateBy(SecurityUtils.getUsername()); + return toAjax(configService.insertConfig(config)); + } + + /** + * 修改参数配置 + */ + @RequiresPermissions("system:config:edit") + @Log(title = "参数管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysConfig config) + { + if (!configService.checkConfigKeyUnique(config)) + { + return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(configService.updateConfig(config)); + } + + /** + * 删除参数配置 + */ + @RequiresPermissions("system:config:remove") + @Log(title = "参数管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{configIds}") + public AjaxResult remove(@PathVariable Long[] configIds) + { + configService.deleteConfigByIds(configIds); + return success(); + } + + /** + * 刷新参数缓存 + */ + @RequiresPermissions("system:config:remove") + @Log(title = "参数管理", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + configService.resetConfigCache(); + return success(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysDeptController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysDeptController.java new file mode 100644 index 0000000..b80b6a6 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysDeptController.java @@ -0,0 +1,133 @@ +package com.storm.system.controller; + +import java.util.List; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysDept; +import com.storm.system.service.ISysDeptService; + +/** + * 部门信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/dept") +public class SysDeptController extends BaseController +{ + @Autowired + private ISysDeptService deptService; + + /** + * 获取部门列表 + */ + @RequiresPermissions("system:dept:list") + @GetMapping("/list") + public AjaxResult list(SysDept dept) + { + List depts = deptService.selectDeptList(dept); + return success(depts); + } + + /** + * 查询部门列表(排除节点) + */ + @RequiresPermissions("system:dept:list") + @GetMapping("/list/exclude/{deptId}") + public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) + { + List depts = deptService.selectDeptList(new SysDept()); + depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + "")); + return success(depts); + } + + /** + * 根据部门编号获取详细信息 + */ + @RequiresPermissions("system:dept:query") + @GetMapping(value = "/{deptId}") + public AjaxResult getInfo(@PathVariable Long deptId) + { + deptService.checkDeptDataScope(deptId); + return success(deptService.selectDeptById(deptId)); + } + + /** + * 新增部门 + */ + @RequiresPermissions("system:dept:add") + @Log(title = "部门管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDept dept) + { + if (!deptService.checkDeptNameUnique(dept)) + { + return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + dept.setCreateBy(SecurityUtils.getUsername()); + return toAjax(deptService.insertDept(dept)); + } + + /** + * 修改部门 + */ + @RequiresPermissions("system:dept:edit") + @Log(title = "部门管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDept dept) + { + Long deptId = dept.getDeptId(); + deptService.checkDeptDataScope(deptId); + if (!deptService.checkDeptNameUnique(dept)) + { + return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + else if (dept.getParentId().equals(deptId)) + { + return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); + } + else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0) + { + return error("该部门包含未停用的子部门!"); + } + dept.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(deptService.updateDept(dept)); + } + + /** + * 删除部门 + */ + @RequiresPermissions("system:dept:remove") + @Log(title = "部门管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{deptId}") + public AjaxResult remove(@PathVariable Long deptId) + { + if (deptService.hasChildByDeptId(deptId)) + { + return warn("存在下级部门,不允许删除"); + } + if (deptService.checkDeptExistUser(deptId)) + { + return warn("部门存在用户,不允许删除"); + } + deptService.checkDeptDataScope(deptId); + return toAjax(deptService.deleteDeptById(deptId)); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysDictDataController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysDictDataController.java new file mode 100644 index 0000000..468540b --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysDictDataController.java @@ -0,0 +1,122 @@ +package com.storm.system.controller; + +import java.util.ArrayList; +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysDictData; +import com.storm.system.service.ISysDictDataService; +import com.storm.system.service.ISysDictTypeService; + +/** + * 数据字典信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/dict/data") +public class SysDictDataController extends BaseController +{ + @Autowired + private ISysDictDataService dictDataService; + + @Autowired + private ISysDictTypeService dictTypeService; + + @RequiresPermissions("system:dict:list") + @GetMapping("/list") + public TableDataInfo list(SysDictData dictData) + { + startPage(); + List list = dictDataService.selectDictDataList(dictData); + return getDataTable(list); + } + + @Log(title = "字典数据", businessType = BusinessType.EXPORT) + @RequiresPermissions("system:dict:export") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictData dictData) + { + List list = dictDataService.selectDictDataList(dictData); + ExcelUtil util = new ExcelUtil(SysDictData.class); + util.exportExcel(response, list, "字典数据"); + } + + /** + * 查询字典数据详细 + */ + @RequiresPermissions("system:dict:query") + @GetMapping(value = "/{dictCode}") + public AjaxResult getInfo(@PathVariable Long dictCode) + { + return success(dictDataService.selectDictDataById(dictCode)); + } + + /** + * 根据字典类型查询字典数据信息 + */ + @GetMapping(value = "/type/{dictType}") + public AjaxResult dictType(@PathVariable String dictType) + { + List data = dictTypeService.selectDictDataByType(dictType); + if (StringUtils.isNull(data)) + { + data = new ArrayList(); + } + return success(data); + } + + /** + * 新增字典类型 + */ + @RequiresPermissions("system:dict:add") + @Log(title = "字典数据", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictData dict) + { + dict.setCreateBy(SecurityUtils.getUsername()); + return toAjax(dictDataService.insertDictData(dict)); + } + + /** + * 修改保存字典类型 + */ + @RequiresPermissions("system:dict:edit") + @Log(title = "字典数据", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictData dict) + { + dict.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(dictDataService.updateDictData(dict)); + } + + /** + * 删除字典类型 + */ + @RequiresPermissions("system:dict:remove") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictCodes}") + public AjaxResult remove(@PathVariable Long[] dictCodes) + { + dictDataService.deleteDictDataByIds(dictCodes); + return success(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysDictTypeController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysDictTypeController.java new file mode 100644 index 0000000..b807036 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysDictTypeController.java @@ -0,0 +1,132 @@ +package com.storm.system.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysDictType; +import com.storm.system.service.ISysDictTypeService; + +/** + * 数据字典信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/dict/type") +public class SysDictTypeController extends BaseController +{ + @Autowired + private ISysDictTypeService dictTypeService; + + @RequiresPermissions("system:dict:list") + @GetMapping("/list") + public TableDataInfo list(SysDictType dictType) + { + startPage(); + List list = dictTypeService.selectDictTypeList(dictType); + return getDataTable(list); + } + + @Log(title = "字典类型", businessType = BusinessType.EXPORT) + @RequiresPermissions("system:dict:export") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictType dictType) + { + List list = dictTypeService.selectDictTypeList(dictType); + ExcelUtil util = new ExcelUtil(SysDictType.class); + util.exportExcel(response, list, "字典类型"); + } + + /** + * 查询字典类型详细 + */ + @RequiresPermissions("system:dict:query") + @GetMapping(value = "/{dictId}") + public AjaxResult getInfo(@PathVariable Long dictId) + { + return success(dictTypeService.selectDictTypeById(dictId)); + } + + /** + * 新增字典类型 + */ + @RequiresPermissions("system:dict:add") + @Log(title = "字典类型", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictType dict) + { + if (!dictTypeService.checkDictTypeUnique(dict)) + { + return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setCreateBy(SecurityUtils.getUsername()); + return toAjax(dictTypeService.insertDictType(dict)); + } + + /** + * 修改字典类型 + */ + @RequiresPermissions("system:dict:edit") + @Log(title = "字典类型", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictType dict) + { + if (!dictTypeService.checkDictTypeUnique(dict)) + { + return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(dictTypeService.updateDictType(dict)); + } + + /** + * 删除字典类型 + */ + @RequiresPermissions("system:dict:remove") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictIds}") + public AjaxResult remove(@PathVariable Long[] dictIds) + { + dictTypeService.deleteDictTypeByIds(dictIds); + return success(); + } + + /** + * 刷新字典缓存 + */ + @RequiresPermissions("system:dict:remove") + @Log(title = "字典类型", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + dictTypeService.resetDictCache(); + return success(); + } + + /** + * 获取字典选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List dictTypes = dictTypeService.selectDictTypeAll(); + return success(dictTypes); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysLogininforController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysLogininforController.java new file mode 100644 index 0000000..4774b66 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysLogininforController.java @@ -0,0 +1,92 @@ +package com.storm.system.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.constant.CacheConstants; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.redis.service.RedisService; +import com.storm.common.security.annotation.InnerAuth; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.system.api.domain.SysLogininfor; +import com.storm.system.service.ISysLogininforService; + +/** + * 系统访问记录 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/logininfor") +public class SysLogininforController extends BaseController +{ + @Autowired + private ISysLogininforService logininforService; + + @Autowired + private RedisService redisService; + + @RequiresPermissions("system:logininfor:list") + @GetMapping("/list") + public TableDataInfo list(SysLogininfor logininfor) + { + startPage(); + List list = logininforService.selectLogininforList(logininfor); + return getDataTable(list); + } + + @Log(title = "登录日志", businessType = BusinessType.EXPORT) + @RequiresPermissions("system:logininfor:export") + @PostMapping("/export") + public void export(HttpServletResponse response, SysLogininfor logininfor) + { + List list = logininforService.selectLogininforList(logininfor); + ExcelUtil util = new ExcelUtil(SysLogininfor.class); + util.exportExcel(response, list, "登录日志"); + } + + @RequiresPermissions("system:logininfor:remove") + @Log(title = "登录日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{infoIds}") + public AjaxResult remove(@PathVariable Long[] infoIds) + { + return toAjax(logininforService.deleteLogininforByIds(infoIds)); + } + + @RequiresPermissions("system:logininfor:remove") + @Log(title = "登录日志", businessType = BusinessType.DELETE) + @DeleteMapping("/clean") + public AjaxResult clean() + { + logininforService.cleanLogininfor(); + return success(); + } + + @RequiresPermissions("system:logininfor:unlock") + @Log(title = "账户解锁", businessType = BusinessType.OTHER) + @GetMapping("/unlock/{userName}") + public AjaxResult unlock(@PathVariable("userName") String userName) + { + redisService.deleteObject(CacheConstants.PWD_ERR_CNT_KEY + userName); + return success(); + } + + @InnerAuth + @PostMapping + public AjaxResult add(@RequestBody SysLogininfor logininfor) + { + return toAjax(logininforService.insertLogininfor(logininfor)); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysMenuController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysMenuController.java new file mode 100644 index 0000000..4601153 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysMenuController.java @@ -0,0 +1,159 @@ +package com.storm.system.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.domain.SysMenu; +import com.storm.system.service.ISysMenuService; + +/** + * 菜单信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/menu") +public class SysMenuController extends BaseController +{ + @Autowired + private ISysMenuService menuService; + + /** + * 获取菜单列表 + */ + @RequiresPermissions("system:menu:list") + @GetMapping("/list") + public AjaxResult list(SysMenu menu) + { + Long userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuList(menu, userId); + return success(menus); + } + + /** + * 根据菜单编号获取详细信息 + */ + @RequiresPermissions("system:menu:query") + @GetMapping(value = "/{menuId}") + public AjaxResult getInfo(@PathVariable Long menuId) + { + return success(menuService.selectMenuById(menuId)); + } + + /** + * 获取菜单下拉树列表 + */ + @GetMapping("/treeselect") + public AjaxResult treeselect(SysMenu menu) + { + Long userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuList(menu, userId); + return success(menuService.buildMenuTreeSelect(menus)); + } + + /** + * 加载对应角色菜单列表树 + */ + @GetMapping(value = "/roleMenuTreeselect/{roleId}") + public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId) + { + Long userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuList(userId); + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId)); + ajax.put("menus", menuService.buildMenuTreeSelect(menus)); + return ajax; + } + + /** + * 新增菜单 + */ + @RequiresPermissions("system:menu:add") + @Log(title = "菜单管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysMenu menu) + { + if (!menuService.checkMenuNameUnique(menu)) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + menu.setCreateBy(SecurityUtils.getUsername()); + return toAjax(menuService.insertMenu(menu)); + } + + /** + * 修改菜单 + */ + @RequiresPermissions("system:menu:edit") + @Log(title = "菜单管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysMenu menu) + { + if (!menuService.checkMenuNameUnique(menu)) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + else if (menu.getMenuId().equals(menu.getParentId())) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); + } + menu.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(menuService.updateMenu(menu)); + } + + /** + * 删除菜单 + */ + @RequiresPermissions("system:menu:remove") + @Log(title = "菜单管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{menuId}") + public AjaxResult remove(@PathVariable("menuId") Long menuId) + { + if (menuService.hasChildByMenuId(menuId)) + { + return warn("存在子菜单,不允许删除"); + } + if (menuService.checkMenuExistRole(menuId)) + { + return warn("菜单已分配,不允许删除"); + } + return toAjax(menuService.deleteMenuById(menuId)); + } + + /** + * 获取路由信息 + * + * @return 路由信息 + */ + @GetMapping("getRouters") + public AjaxResult getRouters() + { + Long userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuTreeByUserId(userId); + return success(menuService.buildMenus(menus)); + } +} \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysNoticeController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysNoticeController.java new file mode 100644 index 0000000..b8b0ac2 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysNoticeController.java @@ -0,0 +1,92 @@ +package com.storm.system.controller; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.domain.SysNotice; +import com.storm.system.service.ISysNoticeService; + +/** + * 公告 信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/notice") +public class SysNoticeController extends BaseController +{ + @Autowired + private ISysNoticeService noticeService; + + /** + * 获取通知公告列表 + */ + @RequiresPermissions("system:notice:list") + @GetMapping("/list") + public TableDataInfo list(SysNotice notice) + { + startPage(); + List list = noticeService.selectNoticeList(notice); + return getDataTable(list); + } + + /** + * 根据通知公告编号获取详细信息 + */ + @RequiresPermissions("system:notice:query") + @GetMapping(value = "/{noticeId}") + public AjaxResult getInfo(@PathVariable Long noticeId) + { + return success(noticeService.selectNoticeById(noticeId)); + } + + /** + * 新增通知公告 + */ + @RequiresPermissions("system:notice:add") + @Log(title = "通知公告", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysNotice notice) + { + notice.setCreateBy(SecurityUtils.getUsername()); + return toAjax(noticeService.insertNotice(notice)); + } + + /** + * 修改通知公告 + */ + @RequiresPermissions("system:notice:edit") + @Log(title = "通知公告", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysNotice notice) + { + notice.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(noticeService.updateNotice(notice)); + } + + /** + * 删除通知公告 + */ + @RequiresPermissions("system:notice:remove") + @Log(title = "通知公告", businessType = BusinessType.DELETE) + @DeleteMapping("/{noticeIds}") + public AjaxResult remove(@PathVariable Long[] noticeIds) + { + return toAjax(noticeService.deleteNoticeByIds(noticeIds)); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysOperlogController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysOperlogController.java new file mode 100644 index 0000000..8204fc4 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysOperlogController.java @@ -0,0 +1,78 @@ +package com.storm.system.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.InnerAuth; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.system.api.domain.SysOperLog; +import com.storm.system.service.ISysOperLogService; + +/** + * 操作日志记录 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/operlog") +public class SysOperlogController extends BaseController +{ + @Autowired + private ISysOperLogService operLogService; + + @RequiresPermissions("system:operlog:list") + @GetMapping("/list") + public TableDataInfo list(SysOperLog operLog) + { + startPage(); + List list = operLogService.selectOperLogList(operLog); + return getDataTable(list); + } + + @Log(title = "操作日志", businessType = BusinessType.EXPORT) + @RequiresPermissions("system:operlog:export") + @PostMapping("/export") + public void export(HttpServletResponse response, SysOperLog operLog) + { + List list = operLogService.selectOperLogList(operLog); + ExcelUtil util = new ExcelUtil(SysOperLog.class); + util.exportExcel(response, list, "操作日志"); + } + + @Log(title = "操作日志", businessType = BusinessType.DELETE) + @RequiresPermissions("system:operlog:remove") + @DeleteMapping("/{operIds}") + public AjaxResult remove(@PathVariable Long[] operIds) + { + return toAjax(operLogService.deleteOperLogByIds(operIds)); + } + + @RequiresPermissions("system:operlog:remove") + @Log(title = "操作日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + operLogService.cleanOperLog(); + return success(); + } + + @InnerAuth + @PostMapping + public AjaxResult add(@RequestBody SysOperLog operLog) + { + return toAjax(operLogService.insertOperlog(operLog)); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysPostController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysPostController.java new file mode 100644 index 0000000..6083628 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysPostController.java @@ -0,0 +1,130 @@ +package com.storm.system.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.domain.SysPost; +import com.storm.system.service.ISysPostService; + +/** + * 岗位信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/post") +public class SysPostController extends BaseController +{ + @Autowired + private ISysPostService postService; + + /** + * 获取岗位列表 + */ + @RequiresPermissions("system:post:list") + @GetMapping("/list") + public TableDataInfo list(SysPost post) + { + startPage(); + List list = postService.selectPostList(post); + return getDataTable(list); + } + + @Log(title = "岗位管理", businessType = BusinessType.EXPORT) + @RequiresPermissions("system:post:export") + @PostMapping("/export") + public void export(HttpServletResponse response, SysPost post) + { + List list = postService.selectPostList(post); + ExcelUtil util = new ExcelUtil(SysPost.class); + util.exportExcel(response, list, "岗位数据"); + } + + /** + * 根据岗位编号获取详细信息 + */ + @RequiresPermissions("system:post:query") + @GetMapping(value = "/{postId}") + public AjaxResult getInfo(@PathVariable Long postId) + { + return success(postService.selectPostById(postId)); + } + + /** + * 新增岗位 + */ + @RequiresPermissions("system:post:add") + @Log(title = "岗位管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysPost post) + { + if (!postService.checkPostNameUnique(post)) + { + return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (!postService.checkPostCodeUnique(post)) + { + return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setCreateBy(SecurityUtils.getUsername()); + return toAjax(postService.insertPost(post)); + } + + /** + * 修改岗位 + */ + @RequiresPermissions("system:post:edit") + @Log(title = "岗位管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysPost post) + { + if (!postService.checkPostNameUnique(post)) + { + return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (!postService.checkPostCodeUnique(post)) + { + return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(postService.updatePost(post)); + } + + /** + * 删除岗位 + */ + @RequiresPermissions("system:post:remove") + @Log(title = "岗位管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{postIds}") + public AjaxResult remove(@PathVariable Long[] postIds) + { + return toAjax(postService.deletePostByIds(postIds)); + } + + /** + * 获取岗位选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List posts = postService.selectPostAll(); + return success(posts); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysProfileController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysProfileController.java new file mode 100644 index 0000000..ee77c38 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysProfileController.java @@ -0,0 +1,163 @@ +package com.storm.system.controller; + +import java.util.Arrays; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.storm.common.core.domain.R; +import com.storm.common.core.utils.DateUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.file.FileTypeUtils; +import com.storm.common.core.utils.file.MimeTypeUtils; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.service.TokenService; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.RemoteFileService; +import com.storm.system.api.domain.SysFile; +import com.storm.system.api.domain.SysUser; +import com.storm.system.api.model.LoginUser; +import com.storm.system.service.ISysUserService; + +/** + * 个人信息 业务处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/user/profile") +public class SysProfileController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private TokenService tokenService; + + @Autowired + private RemoteFileService remoteFileService; + + /** + * 个人信息 + */ + @GetMapping + public AjaxResult profile() + { + String username = SecurityUtils.getUsername(); + SysUser user = userService.selectUserByUserName(username); + AjaxResult ajax = AjaxResult.success(user); + ajax.put("roleGroup", userService.selectUserRoleGroup(username)); + ajax.put("postGroup", userService.selectUserPostGroup(username)); + return ajax; + } + + /** + * 修改用户 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult updateProfile(@RequestBody SysUser user) + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + SysUser currentUser = loginUser.getSysUser(); + currentUser.setNickName(user.getNickName()); + currentUser.setEmail(user.getEmail()); + currentUser.setPhonenumber(user.getPhonenumber()); + currentUser.setSex(user.getSex()); + if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser)) + { + return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在"); + } + if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser)) + { + return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在"); + } + if (userService.updateUserProfile(currentUser)) + { + // 更新缓存用户信息 + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改个人信息异常,请联系管理员"); + } + + /** + * 重置密码 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping("/updatePwd") + public AjaxResult updatePwd(@RequestBody Map params) + { + String oldPassword = params.get("oldPassword"); + String newPassword = params.get("newPassword"); + LoginUser loginUser = SecurityUtils.getLoginUser(); + Long userId = loginUser.getUserid(); + String password = loginUser.getSysUser().getPassword(); + if (!SecurityUtils.matchesPassword(oldPassword, password)) + { + return error("修改密码失败,旧密码错误"); + } + if (SecurityUtils.matchesPassword(newPassword, password)) + { + return error("新密码不能与旧密码相同"); + } + newPassword = SecurityUtils.encryptPassword(newPassword); + if (userService.resetUserPwd(userId, newPassword) > 0) + { + // 更新缓存用户密码&密码最后更新时间 + loginUser.getSysUser().setPwdUpdateDate(DateUtils.getNowDate()); + loginUser.getSysUser().setPassword(newPassword); + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改密码异常,请联系管理员"); + } + + /** + * 头像上传 + */ + @Log(title = "用户头像", businessType = BusinessType.UPDATE) + @PostMapping("/avatar") + public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) + { + if (!file.isEmpty()) + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + String extension = FileTypeUtils.getExtension(file); + if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) + { + return error("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式"); + } + R fileResult = remoteFileService.upload(file); + if (StringUtils.isNull(fileResult) || StringUtils.isNull(fileResult.getData())) + { + return error("文件服务异常,请联系管理员"); + } + String url = fileResult.getData().getUrl(); + if (userService.updateUserAvatar(loginUser.getUserid(), url)) + { + String oldAvatarUrl = loginUser.getSysUser().getAvatar(); + if (StringUtils.isNotEmpty(oldAvatarUrl)) + { + remoteFileService.delete(oldAvatarUrl); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("imgUrl", url); + // 更新缓存用户头像 + loginUser.getSysUser().setAvatar(url); + tokenService.setLoginUser(loginUser); + return ajax; + } + } + return error("上传图片异常,请联系管理员"); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysRoleController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysRoleController.java new file mode 100644 index 0000000..151bbc0 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysRoleController.java @@ -0,0 +1,239 @@ +package com.storm.system.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysDept; +import com.storm.system.api.domain.SysRole; +import com.storm.system.api.domain.SysUser; +import com.storm.system.domain.SysUserRole; +import com.storm.system.service.ISysDeptService; +import com.storm.system.service.ISysRoleService; +import com.storm.system.service.ISysUserService; + +/** + * 角色信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/role") +public class SysRoleController extends BaseController +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysDeptService deptService; + + @RequiresPermissions("system:role:list") + @GetMapping("/list") + public TableDataInfo list(SysRole role) + { + startPage(); + List list = roleService.selectRoleList(role); + return getDataTable(list); + } + + @Log(title = "角色管理", businessType = BusinessType.EXPORT) + @RequiresPermissions("system:role:export") + @PostMapping("/export") + public void export(HttpServletResponse response, SysRole role) + { + List list = roleService.selectRoleList(role); + ExcelUtil util = new ExcelUtil(SysRole.class); + util.exportExcel(response, list, "角色数据"); + } + + /** + * 根据角色编号获取详细信息 + */ + @RequiresPermissions("system:role:query") + @GetMapping(value = "/{roleId}") + public AjaxResult getInfo(@PathVariable Long roleId) + { + roleService.checkRoleDataScope(roleId); + return success(roleService.selectRoleById(roleId)); + } + + /** + * 新增角色 + */ + @RequiresPermissions("system:role:add") + @Log(title = "角色管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysRole role) + { + if (!roleService.checkRoleNameUnique(role)) + { + return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (!roleService.checkRoleKeyUnique(role)) + { + return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setCreateBy(SecurityUtils.getUsername()); + return toAjax(roleService.insertRole(role)); + + } + + /** + * 修改保存角色 + */ + @RequiresPermissions("system:role:edit") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + if (!roleService.checkRoleNameUnique(role)) + { + return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (!roleService.checkRoleKeyUnique(role)) + { + return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(roleService.updateRole(role)); + } + + /** + * 修改保存数据权限 + */ + @RequiresPermissions("system:role:edit") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/dataScope") + public AjaxResult dataScope(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + return toAjax(roleService.authDataScope(role)); + } + + /** + * 状态修改 + */ + @RequiresPermissions("system:role:edit") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + role.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(roleService.updateRoleStatus(role)); + } + + /** + * 删除角色 + */ + @RequiresPermissions("system:role:remove") + @Log(title = "角色管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{roleIds}") + public AjaxResult remove(@PathVariable Long[] roleIds) + { + return toAjax(roleService.deleteRoleByIds(roleIds)); + } + + /** + * 获取角色选择框列表 + */ + @RequiresPermissions("system:role:query") + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + return success(roleService.selectRoleAll()); + } + /** + * 查询已分配用户角色列表 + */ + @RequiresPermissions("system:role:list") + @GetMapping("/authUser/allocatedList") + public TableDataInfo allocatedList(SysUser user) + { + startPage(); + List list = userService.selectAllocatedList(user); + return getDataTable(list); + } + + /** + * 查询未分配用户角色列表 + */ + @RequiresPermissions("system:role:list") + @GetMapping("/authUser/unallocatedList") + public TableDataInfo unallocatedList(SysUser user) + { + startPage(); + List list = userService.selectUnallocatedList(user); + return getDataTable(list); + } + + /** + * 取消授权用户 + */ + @RequiresPermissions("system:role:edit") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancel") + public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole) + { + return toAjax(roleService.deleteAuthUser(userRole)); + } + + /** + * 批量取消授权用户 + */ + @RequiresPermissions("system:role:edit") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancelAll") + public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds) + { + return toAjax(roleService.deleteAuthUsers(roleId, userIds)); + } + + /** + * 批量选择用户授权 + */ + @RequiresPermissions("system:role:edit") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/selectAll") + public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds) + { + roleService.checkRoleDataScope(roleId); + return toAjax(roleService.insertAuthUsers(roleId, userIds)); + } + + /** + * 获取对应角色部门树列表 + */ + @RequiresPermissions("system:role:query") + @GetMapping(value = "/deptTree/{roleId}") + public AjaxResult deptTree(@PathVariable("roleId") Long roleId) + { + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId)); + ajax.put("depts", deptService.selectDeptTreeList(new SysDept())); + return ajax; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysUserController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysUserController.java new file mode 100644 index 0000000..aad09e3 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysUserController.java @@ -0,0 +1,380 @@ +package com.storm.system.controller; + +import java.io.IOException; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.storm.common.core.domain.R; +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.DateUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.poi.ExcelUtil; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.security.annotation.InnerAuth; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.common.security.service.TokenService; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysDept; +import com.storm.system.api.domain.SysRole; +import com.storm.system.api.domain.SysUser; +import com.storm.system.api.model.LoginUser; +import com.storm.system.service.ISysConfigService; +import com.storm.system.service.ISysDeptService; +import com.storm.system.service.ISysPermissionService; +import com.storm.system.service.ISysPostService; +import com.storm.system.service.ISysRoleService; +import com.storm.system.service.ISysUserService; + +/** + * 用户信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/user") +public class SysUserController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysDeptService deptService; + + @Autowired + private ISysPostService postService; + + @Autowired + private ISysPermissionService permissionService; + + @Autowired + private ISysConfigService configService; + + @Autowired + private TokenService tokenService; + + /** + * 获取用户列表 + */ + @RequiresPermissions("system:user:list") + @GetMapping("/list") + public TableDataInfo list(SysUser user) + { + startPage(); + List list = userService.selectUserList(user); + return getDataTable(list); + } + + @Log(title = "用户管理", businessType = BusinessType.EXPORT) + @RequiresPermissions("system:user:export") + @PostMapping("/export") + public void export(HttpServletResponse response, SysUser user) + { + List list = userService.selectUserList(user); + ExcelUtil util = new ExcelUtil(SysUser.class); + util.exportExcel(response, list, "用户数据"); + } + + @Log(title = "用户管理", businessType = BusinessType.IMPORT) + @RequiresPermissions("system:user:import") + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception + { + ExcelUtil util = new ExcelUtil(SysUser.class); + List userList = util.importExcel(file.getInputStream()); + String operName = SecurityUtils.getUsername(); + String message = userService.importUser(userList, updateSupport, operName); + return success(message); + } + + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) throws IOException + { + ExcelUtil util = new ExcelUtil(SysUser.class); + util.importTemplateExcel(response, "用户数据"); + } + + /** + * 获取当前用户信息 + */ + @InnerAuth + @GetMapping("/info/{username}") + public R info(@PathVariable("username") String username) + { + SysUser sysUser = userService.selectUserByUserName(username); + if (StringUtils.isNull(sysUser)) + { + return R.fail("用户名或密码错误"); + } + // 角色集合 + Set roles = permissionService.getRolePermission(sysUser); + // 权限集合 + Set permissions = permissionService.getMenuPermission(sysUser); + LoginUser sysUserVo = new LoginUser(); + sysUserVo.setSysUser(sysUser); + sysUserVo.setRoles(roles); + sysUserVo.setPermissions(permissions); + return R.ok(sysUserVo); + } + + /** + * 注册用户信息 + */ + @InnerAuth + @PostMapping("/register") + public R register(@RequestBody SysUser sysUser) + { + String username = sysUser.getUserName(); + if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) + { + return R.fail("当前系统没有开启注册功能!"); + } + if (!userService.checkUserNameUnique(sysUser)) + { + return R.fail("保存用户'" + username + "'失败,注册账号已存在"); + } + return R.ok(userService.registerUser(sysUser)); + } + + /** + *记录用户登录IP地址和登录时间 + */ + @InnerAuth + @PutMapping("/recordlogin") + public R recordlogin(@RequestBody SysUser sysUser) + { + return R.ok(userService.updateUserProfile(sysUser)); + } + + /** + * 获取用户信息 + * + * @return 用户信息 + */ + @GetMapping("getInfo") + public AjaxResult getInfo() + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + SysUser user = loginUser.getSysUser(); + // 角色集合 + Set roles = permissionService.getRolePermission(user); + // 权限集合 + Set permissions = permissionService.getMenuPermission(user); + if (!loginUser.getPermissions().equals(permissions)) + { + loginUser.setPermissions(permissions); + tokenService.refreshToken(loginUser); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("user", user); + ajax.put("roles", roles); + ajax.put("permissions", permissions); + ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate())); + ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate())); + return ajax; + } + + // 检查初始密码是否提醒修改 + public boolean initPasswordIsModify(Date pwdUpdateDate) + { + Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify")); + return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null; + } + + // 检查密码是否过期 + public boolean passwordIsExpiration(Date pwdUpdateDate) + { + Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays")); + if (passwordValidateDays != null && passwordValidateDays > 0) + { + if (StringUtils.isNull(pwdUpdateDate)) + { + // 如果从未修改过初始密码,直接提醒过期 + return true; + } + Date nowDate = DateUtils.getNowDate(); + return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays; + } + return false; + } + + /** + * 根据用户编号获取详细信息 + */ + @RequiresPermissions("system:user:query") + @GetMapping(value = { "/", "/{userId}" }) + public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) + { + AjaxResult ajax = AjaxResult.success(); + if (StringUtils.isNotNull(userId)) + { + userService.checkUserDataScope(userId); + SysUser sysUser = userService.selectUserById(userId); + ajax.put(AjaxResult.DATA_TAG, sysUser); + ajax.put("postIds", postService.selectPostListByUserId(userId)); + ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList())); + } + List roles = roleService.selectRoleAll(); + ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + ajax.put("posts", postService.selectPostAll()); + return ajax; + } + + /** + * 新增用户 + */ + @RequiresPermissions("system:user:add") + @Log(title = "用户管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysUser user) + { + deptService.checkDeptDataScope(user.getDeptId()); + roleService.checkRoleDataScope(user.getRoleIds()); + if (!userService.checkUserNameUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在"); + } + else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setCreateBy(SecurityUtils.getUsername()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + return toAjax(userService.insertUser(user)); + } + + /** + * 修改用户 + */ + @RequiresPermissions("system:user:edit") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + deptService.checkDeptDataScope(user.getDeptId()); + roleService.checkRoleDataScope(user.getRoleIds()); + if (!userService.checkUserNameUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在"); + } + else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(userService.updateUser(user)); + } + + /** + * 删除用户 + */ + @RequiresPermissions("system:user:remove") + @Log(title = "用户管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{userIds}") + public AjaxResult remove(@PathVariable Long[] userIds) + { + if (ArrayUtils.contains(userIds, SecurityUtils.getUserId())) + { + return error("当前用户不能删除"); + } + return toAjax(userService.deleteUserByIds(userIds)); + } + + /** + * 重置密码 + */ + @RequiresPermissions("system:user:edit") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/resetPwd") + public AjaxResult resetPwd(@RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + user.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(userService.resetPwd(user)); + } + + /** + * 状态修改 + */ + @RequiresPermissions("system:user:edit") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(userService.updateUserStatus(user)); + } + + /** + * 根据用户编号获取授权角色 + */ + @RequiresPermissions("system:user:query") + @GetMapping("/authRole/{userId}") + public AjaxResult authRole(@PathVariable("userId") Long userId) + { + AjaxResult ajax = AjaxResult.success(); + SysUser user = userService.selectUserById(userId); + List roles = roleService.selectRolesByUserId(userId); + ajax.put("user", user); + ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + return ajax; + } + + /** + * 用户授权角色 + */ + @RequiresPermissions("system:user:edit") + @Log(title = "用户管理", businessType = BusinessType.GRANT) + @PutMapping("/authRole") + public AjaxResult insertAuthRole(Long userId, Long[] roleIds) + { + userService.checkUserDataScope(userId); + roleService.checkRoleDataScope(roleIds); + userService.insertUserAuth(userId, roleIds); + return success(); + } + + /** + * 获取部门树列表 + */ + @RequiresPermissions("system:user:list") + @GetMapping("/deptTree") + public AjaxResult deptTree(SysDept dept) + { + return success(deptService.selectDeptTreeList(dept)); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysUserOnlineController.java b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysUserOnlineController.java new file mode 100644 index 0000000..62a8491 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/controller/SysUserOnlineController.java @@ -0,0 +1,83 @@ +package com.storm.system.controller; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.storm.common.core.constant.CacheConstants; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.web.controller.BaseController; +import com.storm.common.core.web.domain.AjaxResult; +import com.storm.common.core.web.page.TableDataInfo; +import com.storm.common.log.annotation.Log; +import com.storm.common.log.enums.BusinessType; +import com.storm.common.redis.service.RedisService; +import com.storm.common.security.annotation.RequiresPermissions; +import com.storm.system.api.model.LoginUser; +import com.storm.system.domain.SysUserOnline; +import com.storm.system.service.ISysUserOnlineService; + +/** + * 在线用户监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/online") +public class SysUserOnlineController extends BaseController +{ + @Autowired + private ISysUserOnlineService userOnlineService; + + @Autowired + private RedisService redisService; + + @RequiresPermissions("monitor:online:list") + @GetMapping("/list") + public TableDataInfo list(String ipaddr, String userName) + { + Collection keys = redisService.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); + List userOnlineList = new ArrayList(); + for (String key : keys) + { + LoginUser user = redisService.getCacheObject(key); + if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) + { + userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user)); + } + else if (StringUtils.isNotEmpty(ipaddr)) + { + userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user)); + } + else if (StringUtils.isNotEmpty(userName)) + { + userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user)); + } + else + { + userOnlineList.add(userOnlineService.loginUserToUserOnline(user)); + } + } + Collections.reverse(userOnlineList); + userOnlineList.removeAll(Collections.singleton(null)); + return getDataTable(userOnlineList); + } + + /** + * 强退用户 + */ + @RequiresPermissions("monitor:online:forceLogout") + @Log(title = "在线用户", businessType = BusinessType.FORCE) + @DeleteMapping("/{tokenId}") + public AjaxResult forceLogout(@PathVariable String tokenId) + { + redisService.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId); + return success(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysConfig.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysConfig.java new file mode 100644 index 0000000..3a8837c --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysConfig.java @@ -0,0 +1,111 @@ +package com.storm.system.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.annotation.Excel.ColumnType; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 参数配置表 sys_config + * + * @author ruoyi + */ +public class SysConfig extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 参数主键 */ + @Excel(name = "参数主键", cellType = ColumnType.NUMERIC) + private Long configId; + + /** 参数名称 */ + @Excel(name = "参数名称") + private String configName; + + /** 参数键名 */ + @Excel(name = "参数键名") + private String configKey; + + /** 参数键值 */ + @Excel(name = "参数键值") + private String configValue; + + /** 系统内置(Y是 N否) */ + @Excel(name = "系统内置", readConverterExp = "Y=是,N=否") + private String configType; + + public Long getConfigId() + { + return configId; + } + + public void setConfigId(Long configId) + { + this.configId = configId; + } + + @NotBlank(message = "参数名称不能为空") + @Size(min = 0, max = 100, message = "参数名称不能超过100个字符") + public String getConfigName() + { + return configName; + } + + public void setConfigName(String configName) + { + this.configName = configName; + } + + @NotBlank(message = "参数键名长度不能为空") + @Size(min = 0, max = 100, message = "参数键名长度不能超过100个字符") + public String getConfigKey() + { + return configKey; + } + + public void setConfigKey(String configKey) + { + this.configKey = configKey; + } + + @NotBlank(message = "参数键值不能为空") + @Size(min = 0, max = 500, message = "参数键值长度不能超过500个字符") + public String getConfigValue() + { + return configValue; + } + + public void setConfigValue(String configValue) + { + this.configValue = configValue; + } + + public String getConfigType() + { + return configType; + } + + public void setConfigType(String configType) + { + this.configType = configType; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("configId", getConfigId()) + .append("configName", getConfigName()) + .append("configKey", getConfigKey()) + .append("configValue", getConfigValue()) + .append("configType", getConfigType()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysMenu.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysMenu.java new file mode 100644 index 0000000..a426110 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysMenu.java @@ -0,0 +1,274 @@ +package com.storm.system.domain; + +import java.util.ArrayList; +import java.util.List; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 菜单权限表 sys_menu + * + * @author ruoyi + */ +public class SysMenu extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 菜单ID */ + private Long menuId; + + /** 菜单名称 */ + private String menuName; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 路由地址 */ + private String path; + + /** 组件路径 */ + private String component; + + /** 路由参数 */ + private String query; + + /** 路由名称,默认和路由地址相同的驼峰格式(注意:因为vue3版本的router会删除名称相同路由,为避免名字的冲突,特殊情况可以自定义) */ + private String routeName; + + /** 是否为外链(0是 1否) */ + private String isFrame; + + /** 是否缓存(0缓存 1不缓存) */ + private String isCache; + + /** 类型(M目录 C菜单 F按钮) */ + private String menuType; + + /** 显示状态(0显示 1隐藏) */ + private String visible; + + /** 菜单状态(0正常 1停用) */ + private String status; + + /** 权限字符串 */ + private String perms; + + /** 菜单图标 */ + private String icon; + + /** 子菜单 */ + private List children = new ArrayList(); + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @NotBlank(message = "菜单名称不能为空") + @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符") + public String getMenuName() + { + return menuName; + } + + public void setMenuName(String menuName) + { + this.menuName = menuName; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + @Size(min = 0, max = 200, message = "路由地址不能超过200个字符") + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + @Size(min = 0, max = 200, message = "组件路径不能超过255个字符") + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public String getRouteName() + { + return routeName; + } + + public void setRouteName(String routeName) + { + this.routeName = routeName; + } + + public String getIsFrame() + { + return isFrame; + } + + public void setIsFrame(String isFrame) + { + this.isFrame = isFrame; + } + + public String getIsCache() + { + return isCache; + } + + public void setIsCache(String isCache) + { + this.isCache = isCache; + } + + @NotBlank(message = "菜单类型不能为空") + public String getMenuType() + { + return menuType; + } + + public void setMenuType(String menuType) + { + this.menuType = menuType; + } + + public String getVisible() + { + return visible; + } + + public void setVisible(String visible) + { + this.visible = visible; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符") + public String getPerms() + { + return perms; + } + + public void setPerms(String perms) + { + this.perms = perms; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("menuId", getMenuId()) + .append("menuName", getMenuName()) + .append("parentId", getParentId()) + .append("orderNum", getOrderNum()) + .append("path", getPath()) + .append("component", getComponent()) + .append("query", getQuery()) + .append("routeName", getRouteName()) + .append("isFrame", getIsFrame()) + .append("IsCache", getIsCache()) + .append("menuType", getMenuType()) + .append("visible", getVisible()) + .append("status ", getStatus()) + .append("perms", getPerms()) + .append("icon", getIcon()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysNotice.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysNotice.java new file mode 100644 index 0000000..7a5e29e --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysNotice.java @@ -0,0 +1,102 @@ +package com.storm.system.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.web.domain.BaseEntity; +import com.storm.common.core.xss.Xss; + +/** + * 通知公告表 sys_notice + * + * @author ruoyi + */ +public class SysNotice extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 公告ID */ + private Long noticeId; + + /** 公告标题 */ + private String noticeTitle; + + /** 公告类型(1通知 2公告) */ + private String noticeType; + + /** 公告内容 */ + private String noticeContent; + + /** 公告状态(0正常 1关闭) */ + private String status; + + public Long getNoticeId() + { + return noticeId; + } + + public void setNoticeId(Long noticeId) + { + this.noticeId = noticeId; + } + + public void setNoticeTitle(String noticeTitle) + { + this.noticeTitle = noticeTitle; + } + + @Xss(message = "公告标题不能包含脚本字符") + @NotBlank(message = "公告标题不能为空") + @Size(min = 0, max = 50, message = "公告标题不能超过50个字符") + public String getNoticeTitle() + { + return noticeTitle; + } + + public void setNoticeType(String noticeType) + { + this.noticeType = noticeType; + } + + public String getNoticeType() + { + return noticeType; + } + + public void setNoticeContent(String noticeContent) + { + this.noticeContent = noticeContent; + } + + public String getNoticeContent() + { + return noticeContent; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("noticeId", getNoticeId()) + .append("noticeTitle", getNoticeTitle()) + .append("noticeType", getNoticeType()) + .append("noticeContent", getNoticeContent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysPost.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysPost.java new file mode 100644 index 0000000..ca214c6 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysPost.java @@ -0,0 +1,124 @@ +package com.storm.system.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.storm.common.core.annotation.Excel; +import com.storm.common.core.annotation.Excel.ColumnType; +import com.storm.common.core.web.domain.BaseEntity; + +/** + * 岗位表 sys_post + * + * @author ruoyi + */ +public class SysPost extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 岗位序号 */ + @Excel(name = "岗位序号", cellType = ColumnType.NUMERIC) + private Long postId; + + /** 岗位编码 */ + @Excel(name = "岗位编码") + private String postCode; + + /** 岗位名称 */ + @Excel(name = "岗位名称") + private String postName; + + /** 岗位排序 */ + @Excel(name = "岗位排序") + private Integer postSort; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 用户是否存在此岗位标识 默认不存在 */ + private boolean flag = false; + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @NotBlank(message = "岗位编码不能为空") + @Size(min = 0, max = 64, message = "岗位编码长度不能超过64个字符") + public String getPostCode() + { + return postCode; + } + + public void setPostCode(String postCode) + { + this.postCode = postCode; + } + + @NotBlank(message = "岗位名称不能为空") + @Size(min = 0, max = 50, message = "岗位名称长度不能超过50个字符") + public String getPostName() + { + return postName; + } + + public void setPostName(String postName) + { + this.postName = postName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getPostSort() + { + return postSort; + } + + public void setPostSort(Integer postSort) + { + this.postSort = postSort; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public boolean isFlag() + { + return flag; + } + + public void setFlag(boolean flag) + { + this.flag = flag; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("postId", getPostId()) + .append("postCode", getPostCode()) + .append("postName", getPostName()) + .append("postSort", getPostSort()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysRoleDept.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysRoleDept.java new file mode 100644 index 0000000..0995e5f --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysRoleDept.java @@ -0,0 +1,46 @@ +package com.storm.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和部门关联 sys_role_dept + * + * @author ruoyi + */ +public class SysRoleDept +{ + /** 角色ID */ + private Long roleId; + + /** 部门ID */ + private Long deptId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("deptId", getDeptId()) + .toString(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysRoleMenu.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysRoleMenu.java new file mode 100644 index 0000000..b531c85 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysRoleMenu.java @@ -0,0 +1,46 @@ +package com.storm.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和菜单关联 sys_role_menu + * + * @author ruoyi + */ +public class SysRoleMenu +{ + /** 角色ID */ + private Long roleId; + + /** 菜单ID */ + private Long menuId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("menuId", getMenuId()) + .toString(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysUserOnline.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysUserOnline.java new file mode 100644 index 0000000..b66d668 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysUserOnline.java @@ -0,0 +1,100 @@ +package com.storm.system.domain; + +/** + * 当前在线会话 + * + * @author ruoyi + */ +public class SysUserOnline +{ + /** 会话编号 */ + private String tokenId; + + /** 用户名称 */ + private String userName; + + /** 登录IP地址 */ + private String ipaddr; + + /** 登录地址 */ + private String loginLocation; + + /** 浏览器类型 */ + private String browser; + + /** 操作系统 */ + private String os; + + /** 登录时间 */ + private Long loginTime; + + public String getTokenId() + { + return tokenId; + } + + public void setTokenId(String tokenId) + { + this.tokenId = tokenId; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysUserPost.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysUserPost.java new file mode 100644 index 0000000..9ca2a15 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysUserPost.java @@ -0,0 +1,46 @@ +package com.storm.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和岗位关联 sys_user_post + * + * @author ruoyi + */ +public class SysUserPost +{ + /** 用户ID */ + private Long userId; + + /** 岗位ID */ + private Long postId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("postId", getPostId()) + .toString(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysUserRole.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysUserRole.java new file mode 100644 index 0000000..40f1492 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/SysUserRole.java @@ -0,0 +1,46 @@ +package com.storm.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和角色关联 sys_user_role + * + * @author ruoyi + */ +public class SysUserRole +{ + /** 用户ID */ + private Long userId; + + /** 角色ID */ + private Long roleId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("roleId", getRoleId()) + .toString(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/vo/MetaVo.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/vo/MetaVo.java new file mode 100644 index 0000000..c57d54b --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/vo/MetaVo.java @@ -0,0 +1,106 @@ +package com.storm.system.domain.vo; + +import com.storm.common.core.utils.StringUtils; + +/** + * 路由显示信息 + * + * @author ruoyi + */ +public class MetaVo +{ + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + private String title; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + private String icon; + + /** + * 设置为true,则不会被 缓存 + */ + private boolean noCache; + + /** + * 内链地址(http(s)://开头) + */ + private String link; + + public MetaVo() + { + } + + public MetaVo(String title, String icon) + { + this.title = title; + this.icon = icon; + } + + public MetaVo(String title, String icon, boolean noCache) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + } + + public MetaVo(String title, String icon, String link) + { + this.title = title; + this.icon = icon; + this.link = link; + } + + public MetaVo(String title, String icon, boolean noCache, String link) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + if (StringUtils.ishttp(link)) + { + this.link = link; + } + } + + public boolean isNoCache() + { + return noCache; + } + + public void setNoCache(boolean noCache) + { + this.noCache = noCache; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public String getLink() + { + return link; + } + + public void setLink(String link) + { + this.link = link; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/vo/RouterVo.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/vo/RouterVo.java new file mode 100644 index 0000000..cc1b157 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/vo/RouterVo.java @@ -0,0 +1,148 @@ +package com.storm.system.domain.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; + +/** + * 路由配置信息 + * + * @author ruoyi + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouterVo +{ + /** + * 路由名字 + */ + private String name; + + /** + * 路由地址 + */ + private String path; + + /** + * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现 + */ + private boolean hidden; + + /** + * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + */ + private String redirect; + + /** + * 组件地址 + */ + private String component; + + /** + * 路由参数:如 {"id": 1, "name": "ry"} + */ + private String query; + + /** + * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + */ + private Boolean alwaysShow; + + /** + * 其他元素 + */ + private MetaVo meta; + + /** + * 子路由 + */ + private List children; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + public boolean getHidden() + { + return hidden; + } + + public void setHidden(boolean hidden) + { + this.hidden = hidden; + } + + public String getRedirect() + { + return redirect; + } + + public void setRedirect(String redirect) + { + this.redirect = redirect; + } + + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public Boolean getAlwaysShow() + { + return alwaysShow; + } + + public void setAlwaysShow(Boolean alwaysShow) + { + this.alwaysShow = alwaysShow; + } + + public MetaVo getMeta() + { + return meta; + } + + public void setMeta(MetaVo meta) + { + this.meta = meta; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/domain/vo/TreeSelect.java b/storm-modules/storm-system/src/main/java/com/storm/system/domain/vo/TreeSelect.java new file mode 100644 index 0000000..a413f6f --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/domain/vo/TreeSelect.java @@ -0,0 +1,93 @@ +package com.storm.system.domain.vo; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.utils.StringUtils; +import com.storm.system.api.domain.SysDept; +import com.storm.system.domain.SysMenu; + +/** + * Treeselect树结构实体类 + * + * @author ruoyi + */ +public class TreeSelect implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 节点ID */ + private Long id; + + /** 节点名称 */ + private String label; + + /** 节点禁用 */ + private boolean disabled = false; + + /** 子节点 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect() + { + + } + + public TreeSelect(SysDept dept) + { + this.id = dept.getDeptId(); + this.label = dept.getDeptName(); + this.disabled = StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()); + this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public TreeSelect(SysMenu menu) + { + this.id = menu.getMenuId(); + this.label = menu.getMenuName(); + this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public boolean isDisabled() + { + return disabled; + } + + public void setDisabled(boolean disabled) + { + this.disabled = disabled; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysConfigMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysConfigMapper.java new file mode 100644 index 0000000..5e7b655 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysConfigMapper.java @@ -0,0 +1,76 @@ +package com.storm.system.mapper; + +import java.util.List; +import com.storm.system.domain.SysConfig; + +/** + * 参数配置 数据层 + * + * @author ruoyi + */ +public interface SysConfigMapper +{ + /** + * 查询参数配置信息 + * + * @param config 参数配置信息 + * @return 参数配置信息 + */ + public SysConfig selectConfig(SysConfig config); + + /** + * 通过ID查询配置 + * + * @param configId 参数ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数配置信息 + */ + public SysConfig checkConfigKeyUnique(String configKey); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 删除参数配置 + * + * @param configId 参数ID + * @return 结果 + */ + public int deleteConfigById(Long configId); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + public int deleteConfigByIds(Long[] configIds); +} \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysDeptMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysDeptMapper.java new file mode 100644 index 0000000..f793d05 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysDeptMapper.java @@ -0,0 +1,118 @@ +package com.storm.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.storm.system.api.domain.SysDept; + +/** + * 部门管理 数据层 + * + * @author ruoyi + */ +public interface SysDeptMapper +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @param deptCheckStrictly 部门树选择项是否关联显示 + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(@Param("roleId") Long roleId, @Param("deptCheckStrictly") boolean deptCheckStrictly); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门 + * + * @param deptId 部门ID + * @return 部门列表 + */ + public List selectChildrenDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public int hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 + */ + public int checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param deptName 部门名称 + * @param parentId 父部门ID + * @return 结果 + */ + public SysDept checkDeptNameUnique(@Param("deptName") String deptName, @Param("parentId") Long parentId); + + /** + * 新增部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 修改所在部门正常状态 + * + * @param deptIds 部门ID组 + */ + public void updateDeptStatusNormal(Long[] deptIds); + + /** + * 修改子元素关系 + * + * @param depts 子元素 + * @return 结果 + */ + public int updateDeptChildren(@Param("depts") List depts); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysDictDataMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysDictDataMapper.java new file mode 100644 index 0000000..686d966 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysDictDataMapper.java @@ -0,0 +1,95 @@ +package com.storm.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.storm.system.api.domain.SysDictData; + +/** + * 字典表 数据层 + * + * @author ruoyi + */ +public interface SysDictDataMapper +{ + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(@Param("dictType") String dictType, @Param("dictValue") String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据 + */ + public int countDictDataByType(String dictType); + + /** + * 通过字典ID删除字典数据信息 + * + * @param dictCode 字典数据ID + * @return 结果 + */ + public int deleteDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + * @return 结果 + */ + public int deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); + + /** + * 同步修改字典类型 + * + * @param oldDictType 旧字典类型 + * @param newDictType 新旧字典类型 + * @return 结果 + */ + public int updateDictDataType(@Param("oldDictType") String oldDictType, @Param("newDictType") String newDictType); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysDictTypeMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysDictTypeMapper.java new file mode 100644 index 0000000..905d57f --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysDictTypeMapper.java @@ -0,0 +1,83 @@ +package com.storm.system.mapper; + +import java.util.List; +import com.storm.system.api.domain.SysDictType; + +/** + * 字典表 数据层 + * + * @author ruoyi + */ +public interface SysDictTypeMapper +{ + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 通过字典ID删除字典信息 + * + * @param dictId 字典ID + * @return 结果 + */ + public int deleteDictTypeById(Long dictId); + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + * @return 结果 + */ + public int deleteDictTypeByIds(Long[] dictIds); + + /** + * 新增字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public SysDictType checkDictTypeUnique(String dictType); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysLogininforMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysLogininforMapper.java new file mode 100644 index 0000000..71f8e10 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysLogininforMapper.java @@ -0,0 +1,42 @@ +package com.storm.system.mapper; + +import java.util.List; +import com.storm.system.api.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 数据层 + * + * @author ruoyi + */ +public interface SysLogininforMapper +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public int insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + * + * @return 结果 + */ + public int cleanLogininfor(); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysMenuMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..06fb21e --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysMenuMapper.java @@ -0,0 +1,125 @@ +package com.storm.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.storm.system.domain.SysMenu; + +/** + * 菜单表 数据层 + * + * @author ruoyi + */ +public interface SysMenuMapper +{ + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu); + + /** + * 根据用户所有权限 + * + * @return 权限列表 + */ + public List selectMenuPerms(); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuListByUserId(SysMenu menu); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public List selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public List selectMenuPermsByUserId(Long userId); + + /** + * 根据用户ID查询菜单 + * + * @return 菜单列表 + */ + public List selectMenuTreeAll(); + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @param menuCheckStrictly 菜单树选择项是否关联显示 + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int hasChildByMenuId(Long menuId); + + /** + * 新增菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menuName 菜单名称 + * @param parentId 父菜单ID + * @return 结果 + */ + public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysNoticeMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysNoticeMapper.java new file mode 100644 index 0000000..0dcc0fe --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysNoticeMapper.java @@ -0,0 +1,60 @@ +package com.storm.system.mapper; + +import java.util.List; +import com.storm.system.domain.SysNotice; + +/** + * 通知公告表 数据层 + * + * @author ruoyi + */ +public interface SysNoticeMapper +{ + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 批量删除公告 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysOperLogMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysOperLogMapper.java new file mode 100644 index 0000000..07230e1 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysOperLogMapper.java @@ -0,0 +1,48 @@ +package com.storm.system.mapper; + +import java.util.List; +import com.storm.system.api.domain.SysOperLog; + +/** + * 操作日志 数据层 + * + * @author ruoyi + */ +public interface SysOperLogMapper +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public int insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysPostMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysPostMapper.java new file mode 100644 index 0000000..a798b88 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysPostMapper.java @@ -0,0 +1,99 @@ +package com.storm.system.mapper; + +import java.util.List; +import com.storm.system.domain.SysPost; + +/** + * 岗位信息 数据层 + * + * @author ruoyi + */ +public interface SysPostMapper +{ + /** + * 查询岗位数据集合 + * + * @param post 岗位信息 + * @return 岗位数据集合 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public List selectPostsByUserName(String userName); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 修改岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); + + /** + * 新增岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 校验岗位名称 + * + * @param postName 岗位名称 + * @return 结果 + */ + public SysPost checkPostNameUnique(String postName); + + /** + * 校验岗位编码 + * + * @param postCode 岗位编码 + * @return 结果 + */ + public SysPost checkPostCodeUnique(String postCode); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysRoleDeptMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysRoleDeptMapper.java new file mode 100644 index 0000000..7c5a13b --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysRoleDeptMapper.java @@ -0,0 +1,44 @@ +package com.storm.system.mapper; + +import java.util.List; +import com.storm.system.domain.SysRoleDept; + +/** + * 角色与部门关联表 数据层 + * + * @author ruoyi + */ +public interface SysRoleDeptMapper +{ + /** + * 通过角色ID删除角色和部门关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleDeptByRoleId(Long roleId); + + /** + * 批量删除角色部门关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleDept(Long[] ids); + + /** + * 查询部门使用数量 + * + * @param deptId 部门ID + * @return 结果 + */ + public int selectCountRoleDeptByDeptId(Long deptId); + + /** + * 批量新增角色部门信息 + * + * @param roleDeptList 角色部门列表 + * @return 结果 + */ + public int batchRoleDept(List roleDeptList); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysRoleMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..02ab771 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysRoleMapper.java @@ -0,0 +1,107 @@ +package com.storm.system.mapper; + +import java.util.List; +import com.storm.system.api.domain.SysRole; + +/** + * 角色表 数据层 + * + * @author ruoyi + */ +public interface SysRoleMapper +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 根据用户ID查询角色 + * + * @param userName 用户名 + * @return 角色列表 + */ + public List selectRolesByUserName(String userName); + + /** + * 校验角色名称是否唯一 + * + * @param roleName 角色名称 + * @return 角色信息 + */ + public SysRole checkRoleNameUnique(String roleName); + + /** + * 校验角色权限是否唯一 + * + * @param roleKey 角色权限 + * @return 角色信息 + */ + public SysRole checkRoleKeyUnique(String roleKey); + + /** + * 修改角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 新增角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysRoleMenuMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..6a91930 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,44 @@ +package com.storm.system.mapper; + +import java.util.List; +import com.storm.system.domain.SysRoleMenu; + +/** + * 角色与菜单关联表 数据层 + * + * @author ruoyi + */ +public interface SysRoleMenuMapper +{ + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int checkMenuExistRole(Long menuId); + + /** + * 通过角色ID删除角色和菜单关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleMenuByRoleId(Long roleId); + + /** + * 批量删除角色菜单关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleMenu(Long[] ids); + + /** + * 批量新增角色菜单信息 + * + * @param roleMenuList 角色菜单列表 + * @return 结果 + */ + public int batchRoleMenu(List roleMenuList); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysUserMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..df13ddc --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysUserMapper.java @@ -0,0 +1,127 @@ +package com.storm.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.storm.system.api.domain.SysUser; + +/** + * 用户表 数据层 + * + * @author ruoyi + */ +public interface SysUserMapper +{ + /** + * 根据条件分页查询用户列表 + * + * @param sysUser 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser sysUser); + + /** + * 根据条件分页查询已配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + public int updateUserAvatar(@Param("userId") Long userId, @Param("avatar") String avatar); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(@Param("userId") Long userId, @Param("password") String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public SysUser checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param phonenumber 手机号码 + * @return 结果 + */ + public SysUser checkPhoneUnique(String phonenumber); + + /** + * 校验email是否唯一 + * + * @param email 用户邮箱 + * @return 结果 + */ + public SysUser checkEmailUnique(String email); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysUserPostMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysUserPostMapper.java new file mode 100644 index 0000000..edd15ca --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysUserPostMapper.java @@ -0,0 +1,44 @@ +package com.storm.system.mapper; + +import java.util.List; +import com.storm.system.domain.SysUserPost; + +/** + * 用户与岗位关联表 数据层 + * + * @author ruoyi + */ +public interface SysUserPostMapper +{ + /** + * 通过用户ID删除用户和岗位关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserPostByUserId(Long userId); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 批量删除用户和岗位关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserPost(Long[] ids); + + /** + * 批量新增用户岗位信息 + * + * @param userPostList 用户岗位列表 + * @return 结果 + */ + public int batchUserPost(List userPostList); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysUserRoleMapper.java b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..22c2ed3 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,62 @@ +package com.storm.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.storm.system.domain.SysUserRole; + +/** + * 用户与角色关联表 数据层 + * + * @author ruoyi + */ +public interface SysUserRoleMapper +{ + /** + * 通过用户ID删除用户和角色关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserRoleByUserId(Long userId); + + /** + * 批量删除用户和角色关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserRole(Long[] ids); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 批量新增用户角色信息 + * + * @param userRoleList 用户角色列表 + * @return 结果 + */ + public int batchUserRole(List userRoleList); + + /** + * 删除用户和角色关联信息 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteUserRoleInfo(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int deleteUserRoleInfos(@Param("roleId") Long roleId, @Param("userIds") Long[] userIds); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysConfigService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysConfigService.java new file mode 100644 index 0000000..c312665 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysConfigService.java @@ -0,0 +1,82 @@ +package com.storm.system.service; + +import java.util.List; +import com.storm.system.domain.SysConfig; + +/** + * 参数配置 服务层 + * + * @author ruoyi + */ +public interface ISysConfigService +{ + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数键值 + */ + public String selectConfigByKey(String configKey); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + public void deleteConfigByIds(Long[] configIds); + + /** + * 加载参数缓存数据 + */ + public void loadingConfigCache(); + + /** + * 清空参数缓存数据 + */ + public void clearConfigCache(); + + /** + * 重置参数缓存数据 + */ + public void resetConfigCache(); + + /** + * 校验参数键名是否唯一 + * + * @param config 参数信息 + * @return 结果 + */ + public boolean checkConfigKeyUnique(SysConfig config); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysDeptService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysDeptService.java new file mode 100644 index 0000000..899ce6a --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysDeptService.java @@ -0,0 +1,124 @@ +package com.storm.system.service; + +import java.util.List; +import com.storm.system.api.domain.SysDept; +import com.storm.system.domain.vo.TreeSelect; + +/** + * 部门管理 服务层 + * + * @author ruoyi + */ +public interface ISysDeptService +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + public List selectDeptTreeList(SysDept dept); + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + public List buildDeptTree(List depts); + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + public List buildDeptTreeSelect(List depts); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(Long roleId); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在部门子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public boolean hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + public boolean checkDeptNameUnique(SysDept dept); + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + public void checkDeptDataScope(Long deptId); + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysDictDataService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysDictDataService.java new file mode 100644 index 0000000..27e8f76 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysDictDataService.java @@ -0,0 +1,60 @@ +package com.storm.system.service; + +import java.util.List; +import com.storm.system.api.domain.SysDictData; + +/** + * 字典 业务层 + * + * @author ruoyi + */ +public interface ISysDictDataService +{ + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(String dictType, String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + public void deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysDictTypeService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysDictTypeService.java new file mode 100644 index 0000000..fa6c331 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysDictTypeService.java @@ -0,0 +1,98 @@ +package com.storm.system.service; + +import java.util.List; +import com.storm.system.api.domain.SysDictData; +import com.storm.system.api.domain.SysDictType; + +/** + * 字典 业务层 + * + * @author ruoyi + */ +public interface ISysDictTypeService +{ + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 批量删除字典信息 + * + * @param dictIds 需要删除的字典ID + */ + public void deleteDictTypeByIds(Long[] dictIds); + + /** + * 加载字典缓存数据 + */ + public void loadingDictCache(); + + /** + * 清空字典缓存数据 + */ + public void clearDictCache(); + + /** + * 重置字典缓存数据 + */ + public void resetDictCache(); + + /** + * 新增保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public boolean checkDictTypeUnique(SysDictType dictType); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysLogininforService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysLogininforService.java new file mode 100644 index 0000000..94fb8f4 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysLogininforService.java @@ -0,0 +1,40 @@ +package com.storm.system.service; + +import java.util.List; +import com.storm.system.api.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 服务层 + * + * @author ruoyi + */ +public interface ISysLogininforService +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public int insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + */ + public void cleanLogininfor(); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysMenuService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysMenuService.java new file mode 100644 index 0000000..b6c545b --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysMenuService.java @@ -0,0 +1,144 @@ +package com.storm.system.service; + +import java.util.List; +import java.util.Set; +import com.storm.system.domain.SysMenu; +import com.storm.system.domain.vo.RouterVo; +import com.storm.system.domain.vo.TreeSelect; + +/** + * 菜单 业务层 + * + * @author ruoyi + */ +public interface ISysMenuService +{ + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(Long userId); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu, Long userId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectMenuPermsByUserId(Long userId); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public Set selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询菜单树信息 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(Long roleId); + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + public List buildMenus(List menus); + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildMenuTree(List menus); + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + public List buildMenuTreeSelect(List menus); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean hasChildByMenuId(Long menuId); + + /** + * 查询菜单是否存在角色 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkMenuExistRole(Long menuId); + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean checkMenuNameUnique(SysMenu menu); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysNoticeService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysNoticeService.java new file mode 100644 index 0000000..7e77bba --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysNoticeService.java @@ -0,0 +1,60 @@ +package com.storm.system.service; + +import java.util.List; +import com.storm.system.domain.SysNotice; + +/** + * 公告 服务层 + * + * @author ruoyi + */ +public interface ISysNoticeService +{ + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 删除公告信息 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysOperLogService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysOperLogService.java new file mode 100644 index 0000000..59c2aca --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysOperLogService.java @@ -0,0 +1,49 @@ +package com.storm.system.service; + +import java.util.List; +import com.storm.system.api.domain.SysOperLog; + +/** + * 操作日志 服务层 + * + * @author ruoyi + */ +public interface ISysOperLogService +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + * @return 结果 + */ + public int insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysPermissionService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysPermissionService.java new file mode 100644 index 0000000..29e8cf5 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysPermissionService.java @@ -0,0 +1,29 @@ +package com.storm.system.service; + +import java.util.Set; + +import com.storm.system.api.domain.SysUser; + +/** + * 权限信息 服务层 + * + * @author ruoyi + */ +public interface ISysPermissionService +{ + /** + * 获取角色数据权限 + * + * @param userId 用户Id + * @return 角色权限信息 + */ + public Set getRolePermission(SysUser user); + + /** + * 获取菜单数据权限 + * + * @param userId 用户Id + * @return 菜单权限信息 + */ + public Set getMenuPermission(SysUser user); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysPostService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysPostService.java new file mode 100644 index 0000000..23ed59d --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysPostService.java @@ -0,0 +1,99 @@ +package com.storm.system.service; + +import java.util.List; +import com.storm.system.domain.SysPost; + +/** + * 岗位信息 服务层 + * + * @author ruoyi + */ +public interface ISysPostService +{ + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位列表 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 校验岗位名称 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostNameUnique(SysPost post); + + /** + * 校验岗位编码 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostCodeUnique(SysPost post); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysRoleService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysRoleService.java new file mode 100644 index 0000000..657715b --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysRoleService.java @@ -0,0 +1,173 @@ +package com.storm.system.service; + +import java.util.List; +import java.util.Set; +import com.storm.system.api.domain.SysRole; +import com.storm.system.domain.SysUserRole; + +/** + * 角色业务层 + * + * @author ruoyi + */ +public interface ISysRoleService +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色列表 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolesByUserId(Long userId); + + /** + * 根据用户ID查询角色权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleNameUnique(SysRole role); + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleKeyUnique(SysRole role); + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + public void checkRoleAllowed(SysRole role); + + /** + * 校验角色是否有数据权限 + * + * @param roleIds 角色id + */ + public void checkRoleDataScope(Long... roleIds); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRoleStatus(SysRole role); + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int authDataScope(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteAuthUser(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + public int deleteAuthUsers(Long roleId, Long[] userIds); + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int insertAuthUsers(Long roleId, Long[] userIds); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysUserOnlineService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysUserOnlineService.java new file mode 100644 index 0000000..2a1fa54 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysUserOnlineService.java @@ -0,0 +1,48 @@ +package com.storm.system.service; + +import com.storm.system.api.model.LoginUser; +import com.storm.system.domain.SysUserOnline; + +/** + * 在线用户 服务层 + * + * @author ruoyi + */ +public interface ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user); + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user); + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user); + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + public SysUserOnline loginUserToUserOnline(LoginUser user); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysUserService.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysUserService.java new file mode 100644 index 0000000..3278128 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/ISysUserService.java @@ -0,0 +1,206 @@ +package com.storm.system.service; + +import java.util.List; +import com.storm.system.api.domain.SysUser; + +/** + * 用户 业务层 + * + * @author ruoyi + */ +public interface ISysUserService +{ + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser user); + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 根据用户ID查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserRoleGroup(String userName); + + /** + * 根据用户ID查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserPostGroup(String userName); + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkUserNameUnique(SysUser user); + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkPhoneUnique(SysUser user); + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkEmailUnique(SysUser user); + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + public void checkUserAllowed(SysUser user); + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + public void checkUserDataScope(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean registerUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserAuth(Long userId, Long[] roleIds); + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserStatus(SysUser user); + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean updateUserProfile(SysUser user); + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + public boolean updateUserAvatar(Long userId, String avatar); + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + public int resetPwd(SysUser user); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(Long userId, String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + public String importUser(List userList, Boolean isUpdateSupport, String operName); +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysConfigServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..9e1b604 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,213 @@ +package com.storm.system.service.impl; + +import java.util.Collection; +import java.util.List; +import javax.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.common.core.constant.CacheConstants; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.exception.ServiceException; +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.redis.service.RedisService; +import com.storm.system.domain.SysConfig; +import com.storm.system.mapper.SysConfigMapper; +import com.storm.system.service.ISysConfigService; + +/** + * 参数配置 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysConfigServiceImpl implements ISysConfigService +{ + @Autowired + private SysConfigMapper configMapper; + + @Autowired + private RedisService redisService; + + /** + * 项目启动时,初始化参数到缓存 + */ + @PostConstruct + public void init() + { + loadingConfigCache(); + } + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + @Override + public SysConfig selectConfigById(Long configId) + { + SysConfig config = new SysConfig(); + config.setConfigId(configId); + return configMapper.selectConfig(config); + } + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数key + * @return 参数键值 + */ + @Override + public String selectConfigByKey(String configKey) + { + String configValue = Convert.toStr(redisService.getCacheObject(getCacheKey(configKey))); + if (StringUtils.isNotEmpty(configValue)) + { + return configValue; + } + SysConfig config = new SysConfig(); + config.setConfigKey(configKey); + SysConfig retConfig = configMapper.selectConfig(config); + if (StringUtils.isNotNull(retConfig)) + { + redisService.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue()); + return retConfig.getConfigValue(); + } + return StringUtils.EMPTY; + } + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + @Override + public List selectConfigList(SysConfig config) + { + return configMapper.selectConfigList(config); + } + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int insertConfig(SysConfig config) + { + int row = configMapper.insertConfig(config); + if (row > 0) + { + redisService.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int updateConfig(SysConfig config) + { + SysConfig temp = configMapper.selectConfigById(config.getConfigId()); + if (!StringUtils.equals(temp.getConfigKey(), config.getConfigKey())) + { + redisService.deleteObject(getCacheKey(temp.getConfigKey())); + } + + int row = configMapper.updateConfig(config); + if (row > 0) + { + redisService.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + @Override + public void deleteConfigByIds(Long[] configIds) + { + for (Long configId : configIds) + { + SysConfig config = selectConfigById(configId); + if (StringUtils.equals(UserConstants.YES, config.getConfigType())) + { + throw new ServiceException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey())); + } + configMapper.deleteConfigById(configId); + redisService.deleteObject(getCacheKey(config.getConfigKey())); + } + } + + /** + * 加载参数缓存数据 + */ + @Override + public void loadingConfigCache() + { + List configsList = configMapper.selectConfigList(new SysConfig()); + for (SysConfig config : configsList) + { + redisService.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + } + + /** + * 清空参数缓存数据 + */ + @Override + public void clearConfigCache() + { + Collection keys = redisService.keys(CacheConstants.SYS_CONFIG_KEY + "*"); + redisService.deleteObject(keys); + } + + /** + * 重置参数缓存数据 + */ + @Override + public void resetConfigCache() + { + clearConfigCache(); + loadingConfigCache(); + } + + /** + * 校验参数键名是否唯一 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public boolean checkConfigKeyUnique(SysConfig config) + { + Long configId = StringUtils.isNull(config.getConfigId()) ? -1L : config.getConfigId(); + SysConfig info = configMapper.checkConfigKeyUnique(config.getConfigKey()); + if (StringUtils.isNotNull(info) && info.getConfigId().longValue() != configId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + private String getCacheKey(String configKey) + { + return CacheConstants.SYS_CONFIG_KEY + configKey; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysDeptServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysDeptServiceImpl.java new file mode 100644 index 0000000..5289a6e --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,338 @@ +package com.storm.system.service.impl; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.exception.ServiceException; +import com.storm.common.core.text.Convert; +import com.storm.common.core.utils.SpringUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.datascope.annotation.DataScope; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysDept; +import com.storm.system.api.domain.SysRole; +import com.storm.system.api.domain.SysUser; +import com.storm.system.domain.vo.TreeSelect; +import com.storm.system.mapper.SysDeptMapper; +import com.storm.system.mapper.SysRoleMapper; +import com.storm.system.service.ISysDeptService; + +/** + * 部门管理 服务实现 + * + * @author ruoyi + */ +@Service +public class SysDeptServiceImpl implements ISysDeptService +{ + @Autowired + private SysDeptMapper deptMapper; + + @Autowired + private SysRoleMapper roleMapper; + + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + @Override + @DataScope(deptAlias = "d") + public List selectDeptList(SysDept dept) + { + return deptMapper.selectDeptList(dept); + } + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + @Override + public List selectDeptTreeList(SysDept dept) + { + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + return buildDeptTreeSelect(depts); + } + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + @Override + public List buildDeptTree(List depts) + { + List returnList = new ArrayList(); + List tempList = depts.stream().map(SysDept::getDeptId).collect(Collectors.toList()); + for (SysDept dept : depts) + { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(dept.getParentId())) + { + recursionFn(depts, dept); + returnList.add(dept); + } + } + if (returnList.isEmpty()) + { + returnList = depts; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + @Override + public List buildDeptTreeSelect(List depts) + { + List deptTrees = buildDeptTree(depts); + return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + @Override + public List selectDeptListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return deptMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly()); + } + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + @Override + public SysDept selectDeptById(Long deptId) + { + return deptMapper.selectDeptById(deptId); + } + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + @Override + public int selectNormalChildrenDeptById(Long deptId) + { + return deptMapper.selectNormalChildrenDeptById(deptId); + } + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public boolean hasChildByDeptId(Long deptId) + { + int result = deptMapper.hasChildByDeptId(deptId); + return result > 0; + } + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + @Override + public boolean checkDeptExistUser(Long deptId) + { + int result = deptMapper.checkDeptExistUser(deptId); + return result > 0; + } + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public boolean checkDeptNameUnique(SysDept dept) + { + Long deptId = StringUtils.isNull(dept.getDeptId()) ? -1L : dept.getDeptId(); + SysDept info = deptMapper.checkDeptNameUnique(dept.getDeptName(), dept.getParentId()); + if (StringUtils.isNotNull(info) && info.getDeptId().longValue() != deptId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + @Override + public void checkDeptDataScope(Long deptId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId()) && StringUtils.isNotNull(deptId)) + { + SysDept dept = new SysDept(); + dept.setDeptId(deptId); + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + if (StringUtils.isEmpty(depts)) + { + throw new ServiceException("没有权限访问部门数据!"); + } + } + } + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int insertDept(SysDept dept) + { + SysDept info = deptMapper.selectDeptById(dept.getParentId()); + // 如果父节点不为正常状态,则不允许新增子节点 + if (!UserConstants.DEPT_NORMAL.equals(info.getStatus())) + { + throw new ServiceException("部门停用,不允许新增"); + } + dept.setAncestors(info.getAncestors() + "," + dept.getParentId()); + return deptMapper.insertDept(dept); + } + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int updateDept(SysDept dept) + { + SysDept newParentDept = deptMapper.selectDeptById(dept.getParentId()); + SysDept oldDept = deptMapper.selectDeptById(dept.getDeptId()); + if (StringUtils.isNotNull(newParentDept) && StringUtils.isNotNull(oldDept)) + { + String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId(); + String oldAncestors = oldDept.getAncestors(); + dept.setAncestors(newAncestors); + updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors); + } + int result = deptMapper.updateDept(dept); + if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors()) + && !StringUtils.equals("0", dept.getAncestors())) + { + // 如果该部门是启用状态,则启用该部门的所有上级部门 + updateParentDeptStatusNormal(dept); + } + return result; + } + + /** + * 修改该部门的父级部门状态 + * + * @param dept 当前部门 + */ + private void updateParentDeptStatusNormal(SysDept dept) + { + String ancestors = dept.getAncestors(); + Long[] deptIds = Convert.toLongArray(ancestors); + deptMapper.updateDeptStatusNormal(deptIds); + } + + /** + * 修改子元素关系 + * + * @param deptId 被修改的部门ID + * @param newAncestors 新的父ID集合 + * @param oldAncestors 旧的父ID集合 + */ + public void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors) + { + List children = deptMapper.selectChildrenDeptById(deptId); + for (SysDept child : children) + { + child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); + } + if (children.size() > 0) + { + deptMapper.updateDeptChildren(children); + } + } + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public int deleteDeptById(Long deptId) + { + return deptMapper.deleteDeptById(deptId); + } + + /** + * 递归列表 + */ + private void recursionFn(List list, SysDept t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysDept tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysDept t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysDept n = (SysDept) it.next(); + if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getDeptId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysDept t) + { + return getChildList(list, t).size() > 0 ? true : false; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysDictDataServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysDictDataServiceImpl.java new file mode 100644 index 0000000..d34d15c --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysDictDataServiceImpl.java @@ -0,0 +1,111 @@ +package com.storm.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.common.security.utils.DictUtils; +import com.storm.system.api.domain.SysDictData; +import com.storm.system.mapper.SysDictDataMapper; +import com.storm.system.service.ISysDictDataService; + +/** + * 字典 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysDictDataServiceImpl implements ISysDictDataService +{ + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataList(SysDictData dictData) + { + return dictDataMapper.selectDictDataList(dictData); + } + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + @Override + public String selectDictLabel(String dictType, String dictValue) + { + return dictDataMapper.selectDictLabel(dictType, dictValue); + } + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + @Override + public SysDictData selectDictDataById(Long dictCode) + { + return dictDataMapper.selectDictDataById(dictCode); + } + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + @Override + public void deleteDictDataByIds(Long[] dictCodes) + { + for (Long dictCode : dictCodes) + { + SysDictData data = selectDictDataById(dictCode); + dictDataMapper.deleteDictDataById(dictCode); + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + } + + /** + * 新增保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int insertDictData(SysDictData data) + { + int row = dictDataMapper.insertDictData(data); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } + + /** + * 修改保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int updateDictData(SysDictData data) + { + int row = dictDataMapper.updateDictData(data); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysDictTypeServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysDictTypeServiceImpl.java new file mode 100644 index 0000000..6306272 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysDictTypeServiceImpl.java @@ -0,0 +1,223 @@ +package com.storm.system.service.impl; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.exception.ServiceException; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.security.utils.DictUtils; +import com.storm.system.api.domain.SysDictData; +import com.storm.system.api.domain.SysDictType; +import com.storm.system.mapper.SysDictDataMapper; +import com.storm.system.mapper.SysDictTypeMapper; +import com.storm.system.service.ISysDictTypeService; + +/** + * 字典 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysDictTypeServiceImpl implements ISysDictTypeService +{ + @Autowired + private SysDictTypeMapper dictTypeMapper; + + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 项目启动时,初始化字典到缓存 + */ + @PostConstruct + public void init() + { + loadingDictCache(); + } + + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeList(SysDictType dictType) + { + return dictTypeMapper.selectDictTypeList(dictType); + } + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeAll() + { + return dictTypeMapper.selectDictTypeAll(); + } + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataByType(String dictType) + { + List dictDatas = DictUtils.getDictCache(dictType); + if (StringUtils.isNotEmpty(dictDatas)) + { + return dictDatas; + } + dictDatas = dictDataMapper.selectDictDataByType(dictType); + if (StringUtils.isNotEmpty(dictDatas)) + { + DictUtils.setDictCache(dictType, dictDatas); + return dictDatas; + } + return null; + } + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeById(Long dictId) + { + return dictTypeMapper.selectDictTypeById(dictId); + } + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeByType(String dictType) + { + return dictTypeMapper.selectDictTypeByType(dictType); + } + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + */ + @Override + public void deleteDictTypeByIds(Long[] dictIds) + { + for (Long dictId : dictIds) + { + SysDictType dictType = selectDictTypeById(dictId); + if (dictDataMapper.countDictDataByType(dictType.getDictType()) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", dictType.getDictName())); + } + dictTypeMapper.deleteDictTypeById(dictId); + DictUtils.removeDictCache(dictType.getDictType()); + } + } + + /** + * 加载字典缓存数据 + */ + @Override + public void loadingDictCache() + { + SysDictData dictData = new SysDictData(); + dictData.setStatus("0"); + Map> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType)); + for (Map.Entry> entry : dictDataMap.entrySet()) + { + DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList())); + } + } + + /** + * 清空字典缓存数据 + */ + @Override + public void clearDictCache() + { + DictUtils.clearDictCache(); + } + + /** + * 重置字典缓存数据 + */ + @Override + public void resetDictCache() + { + clearDictCache(); + loadingDictCache(); + } + + /** + * 新增保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + public int insertDictType(SysDictType dict) + { + int row = dictTypeMapper.insertDictType(dict); + if (row > 0) + { + DictUtils.setDictCache(dict.getDictType(), null); + } + return row; + } + + /** + * 修改保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateDictType(SysDictType dict) + { + SysDictType oldDict = dictTypeMapper.selectDictTypeById(dict.getDictId()); + dictDataMapper.updateDictDataType(oldDict.getDictType(), dict.getDictType()); + int row = dictTypeMapper.updateDictType(dict); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(dict.getDictType()); + DictUtils.setDictCache(dict.getDictType(), dictDatas); + } + return row; + } + + /** + * 校验字典类型称是否唯一 + * + * @param dict 字典类型 + * @return 结果 + */ + @Override + public boolean checkDictTypeUnique(SysDictType dict) + { + Long dictId = StringUtils.isNull(dict.getDictId()) ? -1L : dict.getDictId(); + SysDictType dictType = dictTypeMapper.checkDictTypeUnique(dict.getDictType()); + if (StringUtils.isNotNull(dictType) && dictType.getDictId().longValue() != dictId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysLogininforServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysLogininforServiceImpl.java new file mode 100644 index 0000000..c925e6a --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysLogininforServiceImpl.java @@ -0,0 +1,65 @@ +package com.storm.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.system.api.domain.SysLogininfor; +import com.storm.system.mapper.SysLogininforMapper; +import com.storm.system.service.ISysLogininforService; + +/** + * 系统访问日志情况信息 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysLogininforServiceImpl implements ISysLogininforService +{ + + @Autowired + private SysLogininforMapper logininforMapper; + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + @Override + public int insertLogininfor(SysLogininfor logininfor) + { + return logininforMapper.insertLogininfor(logininfor); + } + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + @Override + public List selectLogininforList(SysLogininfor logininfor) + { + return logininforMapper.selectLogininforList(logininfor); + } + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + @Override + public int deleteLogininforByIds(Long[] infoIds) + { + return logininforMapper.deleteLogininforByIds(infoIds); + } + + /** + * 清空系统登录日志 + */ + @Override + public void cleanLogininfor() + { + logininforMapper.cleanLogininfor(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysMenuServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..ac9f5d0 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,543 @@ +package com.storm.system.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.common.core.constant.Constants; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysRole; +import com.storm.system.api.domain.SysUser; +import com.storm.system.domain.SysMenu; +import com.storm.system.domain.vo.MetaVo; +import com.storm.system.domain.vo.RouterVo; +import com.storm.system.domain.vo.TreeSelect; +import com.storm.system.mapper.SysMenuMapper; +import com.storm.system.mapper.SysRoleMapper; +import com.storm.system.mapper.SysRoleMenuMapper; +import com.storm.system.service.ISysMenuService; + +/** + * 菜单 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysMenuServiceImpl implements ISysMenuService +{ + public static final String PREMISSION_STRING = "perms[\"{0}\"]"; + + @Autowired + private SysMenuMapper menuMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + @Override + public List selectMenuList(Long userId) + { + return selectMenuList(new SysMenu(), userId); + } + + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + @Override + public List selectMenuList(SysMenu menu, Long userId) + { + List menuList = null; + // 管理员显示所有菜单信息 + if (SysUser.isAdmin(userId)) + { + menuList = menuMapper.selectMenuList(menu); + } + else + { + menu.getParams().put("userId", userId); + menuList = menuMapper.selectMenuListByUserId(menu); + } + return menuList; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByUserId(Long userId) + { + List perms = menuMapper.selectMenuPermsByUserId(userId); + Set permsSet = new HashSet<>(); + for (String perm : perms) + { + if (StringUtils.isNotEmpty(perm)) + { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByRoleId(Long roleId) + { + List perms = menuMapper.selectMenuPermsByRoleId(roleId); + Set permsSet = new HashSet<>(); + for (String perm : perms) + { + if (StringUtils.isNotEmpty(perm)) + { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户名称 + * @return 菜单列表 + */ + @Override + public List selectMenuTreeByUserId(Long userId) + { + List menus = null; + if (SecurityUtils.isAdmin(userId)) + { + menus = menuMapper.selectMenuTreeAll(); + } + else + { + menus = menuMapper.selectMenuTreeByUserId(userId); + } + return getChildPerms(menus, 0); + } + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + @Override + public List selectMenuListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return menuMapper.selectMenuListByRoleId(roleId, role.isMenuCheckStrictly()); + } + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + @Override + public List buildMenus(List menus) + { + List routers = new LinkedList(); + for (SysMenu menu : menus) + { + RouterVo router = new RouterVo(); + router.setHidden("1".equals(menu.getVisible())); + router.setName(getRouteName(menu)); + router.setPath(getRouterPath(menu)); + router.setComponent(getComponent(menu)); + router.setQuery(menu.getQuery()); + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + List cMenus = menu.getChildren(); + if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType())) + { + router.setAlwaysShow(true); + router.setRedirect("noRedirect"); + router.setChildren(buildMenus(cMenus)); + } + else if (isMenuFrame(menu)) + { + router.setMeta(null); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + children.setPath(menu.getPath()); + children.setComponent(menu.getComponent()); + children.setName(getRouteName(menu.getRouteName(), menu.getPath())); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + children.setQuery(menu.getQuery()); + childrenList.add(children); + router.setChildren(childrenList); + } + else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) + { + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon())); + router.setPath("/"); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + String routerPath = innerLinkReplaceEach(menu.getPath()); + children.setPath(routerPath); + children.setComponent(UserConstants.INNER_LINK); + children.setName(getRouteName(menu.getRouteName(), routerPath)); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath())); + childrenList.add(children); + router.setChildren(childrenList); + } + routers.add(router); + } + return routers; + } + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + @Override + public List buildMenuTree(List menus) + { + List returnList = new ArrayList(); + List tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList()); + for (Iterator iterator = menus.iterator(); iterator.hasNext();) + { + SysMenu menu = (SysMenu) iterator.next(); + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(menu.getParentId())) + { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) + { + returnList = menus; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + @Override + public List buildMenuTreeSelect(List menus) + { + List menuTrees = buildMenuTree(menus); + return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + @Override + public SysMenu selectMenuById(Long menuId) + { + return menuMapper.selectMenuById(menuId); + } + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean hasChildByMenuId(Long menuId) + { + int result = menuMapper.hasChildByMenuId(menuId); + return result > 0; + } + + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean checkMenuExistRole(Long menuId) + { + int result = roleMenuMapper.checkMenuExistRole(menuId); + return result > 0; + } + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int insertMenu(SysMenu menu) + { + return menuMapper.insertMenu(menu); + } + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int updateMenu(SysMenu menu) + { + return menuMapper.updateMenu(menu); + } + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public int deleteMenuById(Long menuId) + { + return menuMapper.deleteMenuById(menuId); + } + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public boolean checkMenuNameUnique(SysMenu menu) + { + Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + SysMenu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId()); + if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 获取路由名称 + * + * @param menu 菜单信息 + * @return 路由名称 + */ + public String getRouteName(SysMenu menu) + { + // 非外链并且是一级目录(类型为目录) + if (isMenuFrame(menu)) + { + return StringUtils.EMPTY; + } + return getRouteName(menu.getRouteName(), menu.getPath()); + } + + /** + * 获取路由名称,如没有配置路由名称则取路由地址 + * + * @param name 路由名称 + * @param path 路由地址 + * @return 路由名称(驼峰格式) + */ + public String getRouteName(String name, String path) + { + String routerName = StringUtils.isNotEmpty(name) ? name : path; + return StringUtils.capitalize(routerName); + } + + /** + * 获取路由地址 + * + * @param menu 菜单信息 + * @return 路由地址 + */ + public String getRouterPath(SysMenu menu) + { + String routerPath = menu.getPath(); + // 内链打开外网方式 + if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) + { + routerPath = innerLinkReplaceEach(routerPath); + } + // 非外链并且是一级目录(类型为目录) + if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType()) + && UserConstants.NO_FRAME.equals(menu.getIsFrame())) + { + routerPath = "/" + menu.getPath(); + } + // 非外链并且是一级目录(类型为菜单) + else if (isMenuFrame(menu)) + { + routerPath = "/"; + } + return routerPath; + } + + /** + * 获取组件信息 + * + * @param menu 菜单信息 + * @return 组件信息 + */ + public String getComponent(SysMenu menu) + { + String component = UserConstants.LAYOUT; + if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) + { + component = menu.getComponent(); + } + else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu)) + { + component = UserConstants.INNER_LINK; + } + else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) + { + component = UserConstants.PARENT_VIEW; + } + return component; + } + + /** + * 是否为菜单内部跳转 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isMenuFrame(SysMenu menu) + { + return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType()) + && menu.getIsFrame().equals(UserConstants.NO_FRAME); + } + + /** + * 是否为内链组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isInnerLink(SysMenu menu) + { + return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath()); + } + + /** + * 是否为parent_view组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isParentView(SysMenu menu) + { + return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()); + } + + /** + * 根据父节点的ID获取所有子节点 + * + * @param list 分类表 + * @param parentId 传入的父节点ID + * @return String + */ + public List getChildPerms(List list, int parentId) + { + List returnList = new ArrayList(); + for (Iterator iterator = list.iterator(); iterator.hasNext();) + { + SysMenu t = (SysMenu) iterator.next(); + // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 + if (t.getParentId() == parentId) + { + recursionFn(list, t); + returnList.add(t); + } + } + return returnList; + } + + /** + * 递归列表 + * + * @param list 分类表 + * @param t 子节点 + */ + private void recursionFn(List list, SysMenu t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysMenu tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysMenu t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysMenu n = (SysMenu) it.next(); + if (n.getParentId().longValue() == t.getMenuId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysMenu t) + { + return getChildList(list, t).size() > 0; + } + + /** + * 内链域名特殊字符替换 + * + * @return 替换后的内链域名 + */ + public String innerLinkReplaceEach(String path) + { + return StringUtils.replaceEach(path, new String[] { Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":" }, + new String[] { "", "", "", "/", "/" }); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysNoticeServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysNoticeServiceImpl.java new file mode 100644 index 0000000..02498f6 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysNoticeServiceImpl.java @@ -0,0 +1,92 @@ +package com.storm.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.system.domain.SysNotice; +import com.storm.system.mapper.SysNoticeMapper; +import com.storm.system.service.ISysNoticeService; + +/** + * 公告 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysNoticeServiceImpl implements ISysNoticeService +{ + @Autowired + private SysNoticeMapper noticeMapper; + + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + @Override + public SysNotice selectNoticeById(Long noticeId) + { + return noticeMapper.selectNoticeById(noticeId); + } + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + @Override + public List selectNoticeList(SysNotice notice) + { + return noticeMapper.selectNoticeList(notice); + } + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int insertNotice(SysNotice notice) + { + return noticeMapper.insertNotice(notice); + } + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int updateNotice(SysNotice notice) + { + return noticeMapper.updateNotice(notice); + } + + /** + * 删除公告对象 + * + * @param noticeId 公告ID + * @return 结果 + */ + @Override + public int deleteNoticeById(Long noticeId) + { + return noticeMapper.deleteNoticeById(noticeId); + } + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + @Override + public int deleteNoticeByIds(Long[] noticeIds) + { + return noticeMapper.deleteNoticeByIds(noticeIds); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysOperLogServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysOperLogServiceImpl.java new file mode 100644 index 0000000..e98fb40 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysOperLogServiceImpl.java @@ -0,0 +1,77 @@ +package com.storm.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.system.api.domain.SysOperLog; +import com.storm.system.mapper.SysOperLogMapper; +import com.storm.system.service.ISysOperLogService; + +/** + * 操作日志 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysOperLogServiceImpl implements ISysOperLogService +{ + @Autowired + private SysOperLogMapper operLogMapper; + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + * @return 结果 + */ + @Override + public int insertOperlog(SysOperLog operLog) + { + return operLogMapper.insertOperlog(operLog); + } + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + @Override + public List selectOperLogList(SysOperLog operLog) + { + return operLogMapper.selectOperLogList(operLog); + } + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + @Override + public int deleteOperLogByIds(Long[] operIds) + { + return operLogMapper.deleteOperLogByIds(operIds); + } + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + @Override + public SysOperLog selectOperLogById(Long operId) + { + return operLogMapper.selectOperLogById(operId); + } + + /** + * 清空操作日志 + */ + @Override + public void cleanOperLog() + { + operLogMapper.cleanOperLog(); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysPermissionServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysPermissionServiceImpl.java new file mode 100644 index 0000000..0f260c4 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysPermissionServiceImpl.java @@ -0,0 +1,91 @@ +package com.storm.system.service.impl; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.utils.StringUtils; +import com.storm.system.api.domain.SysRole; +import com.storm.system.api.domain.SysUser; +import com.storm.system.service.ISysMenuService; +import com.storm.system.service.ISysPermissionService; +import com.storm.system.service.ISysRoleService; + +/** + * 用户权限处理 + * + * @author ruoyi + */ +@Service +public class SysPermissionServiceImpl implements ISysPermissionService +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysMenuService menuService; + + /** + * 获取角色数据权限 + * + * @param userId 用户Id + * @return 角色权限信息 + */ + @Override + public Set getRolePermission(SysUser user) + { + Set roles = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + roles.add("admin"); + } + else + { + roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); + } + return roles; + } + + /** + * 获取菜单数据权限 + * + * @param userId 用户Id + * @return 菜单权限信息 + */ + @Override + public Set getMenuPermission(SysUser user) + { + Set perms = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + perms.add("*:*:*"); + } + else + { + List roles = user.getRoles(); + if (!CollectionUtils.isEmpty(roles)) + { + // 多角色设置permissions属性,以便数据权限匹配权限 + for (SysRole role : roles) + { + if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && !role.isAdmin()) + { + Set rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId()); + role.setPermissions(rolePerms); + perms.addAll(rolePerms); + } + } + } + else + { + perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); + } + } + return perms; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysPostServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysPostServiceImpl.java new file mode 100644 index 0000000..28bb03e --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysPostServiceImpl.java @@ -0,0 +1,178 @@ +package com.storm.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.exception.ServiceException; +import com.storm.common.core.utils.StringUtils; +import com.storm.system.domain.SysPost; +import com.storm.system.mapper.SysPostMapper; +import com.storm.system.mapper.SysUserPostMapper; +import com.storm.system.service.ISysPostService; + +/** + * 岗位信息 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysPostServiceImpl implements ISysPostService +{ + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位信息集合 + */ + @Override + public List selectPostList(SysPost post) + { + return postMapper.selectPostList(post); + } + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + @Override + public List selectPostAll() + { + return postMapper.selectPostAll(); + } + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + @Override + public SysPost selectPostById(Long postId) + { + return postMapper.selectPostById(postId); + } + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + @Override + public List selectPostListByUserId(Long userId) + { + return postMapper.selectPostListByUserId(userId); + } + + /** + * 校验岗位名称是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostNameUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostNameUnique(post.getPostName()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验岗位编码是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostCodeUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostCodeUnique(post.getPostCode()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int countUserPostById(Long postId) + { + return userPostMapper.countUserPostById(postId); + } + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int deletePostById(Long postId) + { + return postMapper.deletePostById(postId); + } + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + @Override + public int deletePostByIds(Long[] postIds) + { + for (Long postId : postIds) + { + SysPost post = selectPostById(postId); + if (countUserPostById(postId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", post.getPostName())); + } + } + return postMapper.deletePostByIds(postIds); + } + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int insertPost(SysPost post) + { + return postMapper.insertPost(post); + } + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int updatePost(SysPost post) + { + return postMapper.updatePost(post); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysRoleServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..108a6fc --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,427 @@ +package com.storm.system.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.exception.ServiceException; +import com.storm.common.core.utils.SpringUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.datascope.annotation.DataScope; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysRole; +import com.storm.system.api.domain.SysUser; +import com.storm.system.domain.SysRoleDept; +import com.storm.system.domain.SysRoleMenu; +import com.storm.system.domain.SysUserRole; +import com.storm.system.mapper.SysRoleDeptMapper; +import com.storm.system.mapper.SysRoleMapper; +import com.storm.system.mapper.SysRoleMenuMapper; +import com.storm.system.mapper.SysUserRoleMapper; +import com.storm.system.service.ISysRoleService; + +/** + * 角色 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysRoleServiceImpl implements ISysRoleService +{ + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysRoleDeptMapper roleDeptMapper; + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + @Override + @DataScope(deptAlias = "d") + public List selectRoleList(SysRole role) + { + return roleMapper.selectRoleList(role); + } + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + @Override + public List selectRolesByUserId(Long userId) + { + List userRoles = roleMapper.selectRolePermissionByUserId(userId); + List roles = selectRoleAll(); + for (SysRole role : roles) + { + for (SysRole userRole : userRoles) + { + if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) + { + role.setFlag(true); + break; + } + } + } + return roles; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectRolePermissionByUserId(Long userId) + { + List perms = roleMapper.selectRolePermissionByUserId(userId); + Set permsSet = new HashSet<>(); + for (SysRole perm : perms) + { + if (StringUtils.isNotNull(perm)) + { + permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(","))); + } + } + return permsSet; + } + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + @Override + public List selectRoleAll() + { + return SpringUtils.getAopProxy(this).selectRoleList(new SysRole()); + } + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + @Override + public List selectRoleListByUserId(Long userId) + { + return roleMapper.selectRoleListByUserId(userId); + } + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + @Override + public SysRole selectRoleById(Long roleId) + { + return roleMapper.selectRoleById(roleId); + } + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleNameUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleNameUnique(role.getRoleName()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleKeyUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleKeyUnique(role.getRoleKey()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + @Override + public void checkRoleAllowed(SysRole role) + { + if (StringUtils.isNotNull(role.getRoleId()) && role.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员角色"); + } + } + + /** + * 校验角色是否有数据权限 + * + * @param roleIds 角色id + */ + @Override + public void checkRoleDataScope(Long... roleIds) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + for (Long roleId : roleIds) + { + SysRole role = new SysRole(); + role.setRoleId(roleId); + List roles = SpringUtils.getAopProxy(this).selectRoleList(role); + if (StringUtils.isEmpty(roles)) + { + throw new ServiceException("没有权限访问角色数据!"); + } + } + } + } + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + public int countUserRoleByRoleId(Long roleId) + { + return userRoleMapper.countUserRoleByRoleId(roleId); + } + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertRole(SysRole role) + { + // 新增角色信息 + roleMapper.insertRole(role); + return insertRoleMenu(role); + } + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateRole(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId()); + return insertRoleMenu(role); + } + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public int updateRoleStatus(SysRole role) + { + return roleMapper.updateRole(role); + } + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int authDataScope(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId()); + // 新增角色和部门信息(数据权限) + return insertRoleDept(role); + } + + /** + * 新增角色菜单信息 + * + * @param role 角色对象 + */ + public int insertRoleMenu(SysRole role) + { + int rows = 1; + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long menuId : role.getMenuIds()) + { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(role.getRoleId()); + rm.setMenuId(menuId); + list.add(rm); + } + if (list.size() > 0) + { + rows = roleMenuMapper.batchRoleMenu(list); + } + return rows; + } + + /** + * 新增角色部门信息(数据权限) + * + * @param role 角色对象 + */ + public int insertRoleDept(SysRole role) + { + int rows = 1; + // 新增角色与部门(数据权限)管理 + List list = new ArrayList(); + for (Long deptId : role.getDeptIds()) + { + SysRoleDept rd = new SysRoleDept(); + rd.setRoleId(role.getRoleId()); + rd.setDeptId(deptId); + list.add(rd); + } + if (list.size() > 0) + { + rows = roleDeptMapper.batchRoleDept(list); + } + return rows; + } + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteRoleById(Long roleId) + { + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(roleId); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(roleId); + return roleMapper.deleteRoleById(roleId); + } + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteRoleByIds(Long[] roleIds) + { + for (Long roleId : roleIds) + { + checkRoleAllowed(new SysRole(roleId)); + checkRoleDataScope(roleId); + SysRole role = selectRoleById(roleId); + if (countUserRoleByRoleId(roleId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName())); + } + } + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenu(roleIds); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDept(roleIds); + return roleMapper.deleteRoleByIds(roleIds); + } + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + @Override + public int deleteAuthUser(SysUserRole userRole) + { + return userRoleMapper.deleteUserRoleInfo(userRole); + } + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + @Override + public int deleteAuthUsers(Long roleId, Long[] userIds) + { + return userRoleMapper.deleteUserRoleInfos(roleId, userIds); + } + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要授权的用户数据ID + * @return 结果 + */ + @Override + public int insertAuthUsers(Long roleId, Long[] userIds) + { + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long userId : userIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + return userRoleMapper.batchUserRole(list); + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysUserOnlineServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysUserOnlineServiceImpl.java new file mode 100644 index 0000000..b55d5b8 --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysUserOnlineServiceImpl.java @@ -0,0 +1,89 @@ +package com.storm.system.service.impl; + +import org.springframework.stereotype.Service; +import com.storm.common.core.utils.StringUtils; +import com.storm.system.api.model.LoginUser; +import com.storm.system.domain.SysUserOnline; +import com.storm.system.service.ISysUserOnlineService; + +/** + * 在线用户 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysUserOnlineServiceImpl implements ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user) + { + if (StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + @Override + public SysUserOnline loginUserToUserOnline(LoginUser user) + { + if (StringUtils.isNull(user)) + { + return null; + } + SysUserOnline sysUserOnline = new SysUserOnline(); + sysUserOnline.setTokenId(user.getToken()); + sysUserOnline.setUserName(user.getUsername()); + sysUserOnline.setIpaddr(user.getIpaddr()); + sysUserOnline.setLoginTime(user.getLoginTime()); + return sysUserOnline; + } +} diff --git a/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysUserServiceImpl.java b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..4048f3d --- /dev/null +++ b/storm-modules/storm-system/src/main/java/com/storm/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,551 @@ +package com.storm.system.service.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.validation.Validator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import com.storm.common.core.constant.UserConstants; +import com.storm.common.core.exception.ServiceException; +import com.storm.common.core.utils.SpringUtils; +import com.storm.common.core.utils.StringUtils; +import com.storm.common.core.utils.bean.BeanValidators; +import com.storm.common.datascope.annotation.DataScope; +import com.storm.common.security.utils.SecurityUtils; +import com.storm.system.api.domain.SysRole; +import com.storm.system.api.domain.SysUser; +import com.storm.system.domain.SysPost; +import com.storm.system.domain.SysUserPost; +import com.storm.system.domain.SysUserRole; +import com.storm.system.mapper.SysPostMapper; +import com.storm.system.mapper.SysRoleMapper; +import com.storm.system.mapper.SysUserMapper; +import com.storm.system.mapper.SysUserPostMapper; +import com.storm.system.mapper.SysUserRoleMapper; +import com.storm.system.service.ISysConfigService; +import com.storm.system.service.ISysDeptService; +import com.storm.system.service.ISysUserService; + +/** + * 用户 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysUserServiceImpl implements ISysUserService +{ + private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class); + + @Autowired + private SysUserMapper userMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + private ISysDeptService deptService; + + @Autowired + protected Validator validator; + + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUserList(SysUser user) + { + return userMapper.selectUserList(user); + } + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectAllocatedList(SysUser user) + { + return userMapper.selectAllocatedList(user); + } + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUnallocatedList(SysUser user) + { + return userMapper.selectUnallocatedList(user); + } + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + @Override + public SysUser selectUserByUserName(String userName) + { + return userMapper.selectUserByUserName(userName); + } + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + @Override + public SysUser selectUserById(Long userId) + { + return userMapper.selectUserById(userId); + } + + /** + * 查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserRoleGroup(String userName) + { + List list = roleMapper.selectRolesByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(",")); + } + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserPostGroup(String userName) + { + List list = postMapper.selectPostsByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysPost::getPostName).collect(Collectors.joining(",")); + } + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean checkUserNameUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkUserNameUnique(user.getUserName()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkPhoneUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkEmailUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkEmailUnique(user.getEmail()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + @Override + public void checkUserAllowed(SysUser user) + { + if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员用户"); + } + } + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + @Override + public void checkUserDataScope(Long userId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + SysUser user = new SysUser(); + user.setUserId(userId); + List users = SpringUtils.getAopProxy(this).selectUserList(user); + if (StringUtils.isEmpty(users)) + { + throw new ServiceException("没有权限访问用户数据!"); + } + } + } + + /** + * 新增保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertUser(SysUser user) + { + // 新增用户信息 + int rows = userMapper.insertUser(user); + // 新增用户岗位关联 + insertUserPost(user); + // 新增用户与角色管理 + insertUserRole(user); + return rows; + } + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean registerUser(SysUser user) + { + return userMapper.insertUser(user) > 0; + } + + /** + * 修改保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateUser(SysUser user) + { + Long userId = user.getUserId(); + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 新增用户与角色管理 + insertUserRole(user); + // 删除用户与岗位关联 + userPostMapper.deleteUserPostByUserId(userId); + // 新增用户与岗位管理 + insertUserPost(user); + return userMapper.updateUser(user); + } + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void insertUserAuth(Long userId, Long[] roleIds) + { + userRoleMapper.deleteUserRoleByUserId(userId); + insertUserRole(userId, roleIds); + } + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserStatus(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean updateUserProfile(SysUser user) + { + return userMapper.updateUser(user) > 0; + } + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + @Override + public boolean updateUserAvatar(Long userId, String avatar) + { + return userMapper.updateUserAvatar(userId, avatar) > 0; + } + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int resetPwd(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + @Override + public int resetUserPwd(Long userId, String password) + { + return userMapper.resetUserPwd(userId, password); + } + + /** + * 新增用户角色信息 + * + * @param user 用户对象 + */ + public void insertUserRole(SysUser user) + { + this.insertUserRole(user.getUserId(), user.getRoleIds()); + } + + /** + * 新增用户岗位信息 + * + * @param user 用户对象 + */ + public void insertUserPost(SysUser user) + { + Long[] posts = user.getPostIds(); + if (StringUtils.isNotEmpty(posts)) + { + // 新增用户与岗位管理 + List list = new ArrayList(); + for (Long postId : posts) + { + SysUserPost up = new SysUserPost(); + up.setUserId(user.getUserId()); + up.setPostId(postId); + list.add(up); + } + userPostMapper.batchUserPost(list); + } + } + + /** + * 新增用户角色信息 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserRole(Long userId, Long[] roleIds) + { + if (StringUtils.isNotEmpty(roleIds)) + { + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long roleId : roleIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + userRoleMapper.batchUserRole(list); + } + } + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteUserById(Long userId) + { + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 删除用户与岗位表 + userPostMapper.deleteUserPostByUserId(userId); + return userMapper.deleteUserById(userId); + } + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteUserByIds(Long[] userIds) + { + for (Long userId : userIds) + { + checkUserAllowed(new SysUser(userId)); + checkUserDataScope(userId); + } + // 删除用户与角色关联 + userRoleMapper.deleteUserRole(userIds); + // 删除用户与岗位关联 + userPostMapper.deleteUserPost(userIds); + return userMapper.deleteUserByIds(userIds); + } + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + @Override + public String importUser(List userList, Boolean isUpdateSupport, String operName) + { + if (StringUtils.isNull(userList) || userList.size() == 0) + { + throw new ServiceException("导入用户数据不能为空!"); + } + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + for (SysUser user : userList) + { + try + { + // 验证是否存在这个用户 + SysUser u = userMapper.selectUserByUserName(user.getUserName()); + if (StringUtils.isNull(u)) + { + BeanValidators.validateWithException(validator, user); + deptService.checkDeptDataScope(user.getDeptId()); + String password = configService.selectConfigByKey("sys.user.initPassword"); + user.setPassword(SecurityUtils.encryptPassword(password)); + user.setCreateBy(operName); + userMapper.insertUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 导入成功"); + } + else if (isUpdateSupport) + { + BeanValidators.validateWithException(validator, user); + checkUserAllowed(u); + checkUserDataScope(u.getUserId()); + deptService.checkDeptDataScope(user.getDeptId()); + user.setUserId(u.getUserId()); + user.setUpdateBy(operName); + userMapper.updateUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 更新成功"); + } + else + { + failureNum++; + failureMsg.append("
" + failureNum + "、账号 " + user.getUserName() + " 已存在"); + } + } + catch (Exception e) + { + failureNum++; + String msg = "
" + failureNum + "、账号 " + user.getUserName() + " 导入失败:"; + failureMsg.append(msg + e.getMessage()); + log.error(msg, e); + } + } + if (failureNum > 0) + { + failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); + throw new ServiceException(failureMsg.toString()); + } + else + { + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + } + return successMsg.toString(); + } + +} diff --git a/storm-modules/storm-system/src/main/resources/applocaltion.yml b/storm-modules/storm-system/src/main/resources/applocaltion.yml new file mode 100644 index 0000000..e69de29 diff --git a/storm-modules/storm-system/src/main/resources/banner.txt b/storm-modules/storm-system/src/main/resources/banner.txt new file mode 100644 index 0000000..fbd45f5 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/banner.txt @@ -0,0 +1,10 @@ +Spring Boot Version: ${spring-boot.version} +Spring Application Name: ${spring.application.name} + _ _ + (_) | | + _ __ _ _ ___ _ _ _ ______ ___ _ _ ___ | |_ ___ _ __ ___ +| '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \ +| | | |_| || (_) || |_| || | \__ \| |_| |\__ \| |_ | __/| | | | | | +|_| \__,_| \___/ \__, ||_| |___/ \__, ||___/ \__| \___||_| |_| |_| + __/ | __/ | + |___/ |___/ \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/bootstrap.yml b/storm-modules/storm-system/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..3e54b29 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/bootstrap.yml @@ -0,0 +1,44 @@ +# Tomcat +server: + port: 9201 + +# Spring +spring: + application: + # 应用名称 + name: storm-system + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 127.0.0.1:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: 127.0.0.1:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} +# mybatis-plus配置 +mybatis-plus: + # 搜索指定包别名 + typeAliasesPackage: com.storm.system.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath:mapper/**/*Mapper.xml + # 全局配置 + global-config: + db-config: + id-type: auto #id生成策略为自增 + configuration: + map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名 diff --git a/storm-modules/storm-system/src/main/resources/bootstrap.yml01 b/storm-modules/storm-system/src/main/resources/bootstrap.yml01 new file mode 100644 index 0000000..677c474 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/bootstrap.yml01 @@ -0,0 +1,44 @@ +# Tomcat +server: + port: 9201 + +# Spring +spring: + application: + # 应用名称 + name: storm-system + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: nacos + namespace: public + config: + # 配置中心地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: nacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} +# mybatis-plus配置 +mybatis-plus: + # 搜索指定包别名 + typeAliasesPackage: com.storm.system.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath:mapper/**/*Mapper.xml + # 全局配置 + global-config: + db-config: + id-type: auto #id生成策略为自增 + configuration: + map-underscore-to-camel-case: true #字段与属性,自动转换为驼峰命名 diff --git a/storm-modules/storm-system/src/main/resources/logback.xml b/storm-modules/storm-system/src/main/resources/logback.xml new file mode 100644 index 0000000..f6472a4 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysConfigMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysConfigMapper.xml new file mode 100644 index 0000000..6366660 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysConfigMapper.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark + from sys_config + + + + + + + and config_id = #{configId} + + + and config_key = #{configKey} + + + + + + + + + + + + + + insert into sys_config ( + config_name, + config_key, + config_value, + config_type, + create_by, + remark, + create_time + )values( + #{configName}, + #{configKey}, + #{configValue}, + #{configType}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_config + + config_name = #{configName}, + config_key = #{configKey}, + config_value = #{configValue}, + config_type = #{configType}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where config_id = #{configId} + + + + delete from sys_config where config_id = #{configId} + + + + delete from sys_config where config_id in + + #{configId} + + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysDeptMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysDeptMapper.xml new file mode 100644 index 0000000..e62ed31 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysDeptMapper.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time + from sys_dept d + + + + + + + + + + + + + + + + + + + + insert into sys_dept( + dept_id, + parent_id, + dept_name, + ancestors, + order_num, + leader, + phone, + email, + status, + create_by, + create_time + )values( + #{deptId}, + #{parentId}, + #{deptName}, + #{ancestors}, + #{orderNum}, + #{leader}, + #{phone}, + #{email}, + #{status}, + #{createBy}, + sysdate() + ) + + + + update sys_dept + + parent_id = #{parentId}, + dept_name = #{deptName}, + ancestors = #{ancestors}, + order_num = #{orderNum}, + leader = #{leader}, + phone = #{phone}, + email = #{email}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where dept_id = #{deptId} + + + + update sys_dept set ancestors = + + when #{item.deptId} then #{item.ancestors} + + where dept_id in + + #{item.deptId} + + + + + update sys_dept set status = '0' where dept_id in + + #{deptId} + + + + + update sys_dept set del_flag = '2' where dept_id = #{deptId} + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysDictDataMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysDictDataMapper.xml new file mode 100644 index 0000000..2c28188 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysDictDataMapper.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark + from sys_dict_data + + + + + + + + + + + + + + delete from sys_dict_data where dict_code = #{dictCode} + + + + delete from sys_dict_data where dict_code in + + #{dictCode} + + + + + update sys_dict_data + + dict_sort = #{dictSort}, + dict_label = #{dictLabel}, + dict_value = #{dictValue}, + dict_type = #{dictType}, + css_class = #{cssClass}, + list_class = #{listClass}, + is_default = #{isDefault}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_code = #{dictCode} + + + + update sys_dict_data set dict_type = #{newDictType} where dict_type = #{oldDictType} + + + + insert into sys_dict_data( + dict_sort, + dict_label, + dict_value, + dict_type, + css_class, + list_class, + is_default, + status, + remark, + create_by, + create_time + )values( + #{dictSort}, + #{dictLabel}, + #{dictValue}, + #{dictType}, + #{cssClass}, + #{listClass}, + #{isDefault}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysDictTypeMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysDictTypeMapper.xml new file mode 100644 index 0000000..d81359f --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysDictTypeMapper.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + select dict_id, dict_name, dict_type, status, create_by, create_time, remark + from sys_dict_type + + + + + + + + + + + + + + delete from sys_dict_type where dict_id = #{dictId} + + + + delete from sys_dict_type where dict_id in + + #{dictId} + + + + + update sys_dict_type + + dict_name = #{dictName}, + dict_type = #{dictType}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_id = #{dictId} + + + + insert into sys_dict_type( + dict_name, + dict_type, + status, + remark, + create_by, + create_time + )values( + #{dictName}, + #{dictType}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysLogininforMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysLogininforMapper.xml new file mode 100644 index 0000000..651ae53 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysLogininforMapper.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + insert into sys_logininfor (user_name, status, ipaddr, msg, access_time) + values (#{userName}, #{status}, #{ipaddr}, #{msg}, sysdate()) + + + + + + delete from sys_logininfor where info_id in + + #{infoId} + + + + + truncate table sys_logininfor + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysMenuMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysMenuMapper.xml new file mode 100644 index 0000000..1ebdea6 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysMenuMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time + from sys_menu + + + + + + + + + + + + + + + + + + + + + + + + + + update sys_menu + + menu_name = #{menuName}, + parent_id = #{parentId}, + order_num = #{orderNum}, + path = #{path}, + component = #{component}, + `query` = #{query}, + route_name = #{routeName}, + is_frame = #{isFrame}, + is_cache = #{isCache}, + menu_type = #{menuType}, + visible = #{visible}, + status = #{status}, + perms = #{perms}, + icon = #{icon}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where menu_id = #{menuId} + + + + insert into sys_menu( + menu_id, + parent_id, + menu_name, + order_num, + path, + component, + `query`, + route_name, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + remark, + create_by, + create_time + )values( + #{menuId}, + #{parentId}, + #{menuName}, + #{orderNum}, + #{path}, + #{component}, + #{query}, + #{routeName}, + #{isFrame}, + #{isCache}, + #{menuType}, + #{visible}, + #{status}, + #{perms}, + #{icon}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_menu where menu_id = #{menuId} + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysNoticeMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysNoticeMapper.xml new file mode 100644 index 0000000..8749410 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysNoticeMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark + from sys_notice + + + + + + + + insert into sys_notice ( + notice_title, + notice_type, + notice_content, + status, + remark, + create_by, + create_time + )values( + #{noticeTitle}, + #{noticeType}, + #{noticeContent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_notice + + notice_title = #{noticeTitle}, + notice_type = #{noticeType}, + notice_content = #{noticeContent}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id in + + #{noticeId} + + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysOperLogMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysOperLogMapper.xml new file mode 100644 index 0000000..98ffd74 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysOperLogMapper.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_param, json_result, status, error_msg, oper_time, cost_time + from sys_oper_log + + + + insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_param, json_result, status, error_msg, cost_time, oper_time) + values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, #{costTime}, sysdate()) + + + + + + delete from sys_oper_log where oper_id in + + #{operId} + + + + + + + truncate table sys_oper_log + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysPostMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysPostMapper.xml new file mode 100644 index 0000000..12d1f96 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysPostMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + select post_id, post_code, post_name, post_sort, status, create_by, create_time, remark + from sys_post + + + + + + + + + + + + + + + + + + update sys_post + + post_code = #{postCode}, + post_name = #{postName}, + post_sort = #{postSort}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where post_id = #{postId} + + + + insert into sys_post( + post_id, + post_code, + post_name, + post_sort, + status, + remark, + create_by, + create_time + )values( + #{postId}, + #{postCode}, + #{postName}, + #{postSort}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_post where post_id = #{postId} + + + + delete from sys_post where post_id in + + #{postId} + + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml new file mode 100644 index 0000000..0508580 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_role_dept where role_id=#{roleId} + + + + + + delete from sys_role_dept where role_id in + + #{roleId} + + + + + insert into sys_role_dept(role_id, dept_id) values + + (#{item.roleId},#{item.deptId}) + + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysRoleMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysRoleMapper.xml new file mode 100644 index 0000000..0b36b86 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysRoleMapper.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly, + r.status, r.del_flag, r.create_time, r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join sys_user u on u.user_id = ur.user_id + left join sys_dept d on u.dept_id = d.dept_id + + + + + + + + + + + + + + + + + + + + insert into sys_role( + role_id, + role_name, + role_key, + role_sort, + data_scope, + menu_check_strictly, + dept_check_strictly, + status, + remark, + create_by, + create_time + )values( + #{roleId}, + #{roleName}, + #{roleKey}, + #{roleSort}, + #{dataScope}, + #{menuCheckStrictly}, + #{deptCheckStrictly}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_role + + role_name = #{roleName}, + role_key = #{roleKey}, + role_sort = #{roleSort}, + data_scope = #{dataScope}, + menu_check_strictly = #{menuCheckStrictly}, + dept_check_strictly = #{deptCheckStrictly}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id in + + #{roleId} + + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml new file mode 100644 index 0000000..5b27dc4 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + delete from sys_role_menu where role_id=#{roleId} + + + + delete from sys_role_menu where role_id in + + #{roleId} + + + + + insert into sys_role_menu(role_id, menu_id) values + + (#{item.roleId},#{item.menuId}) + + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysUserMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysUserMapper.xml new file mode 100644 index 0000000..6a6ad1c --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark, + d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, + r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status + from sys_user u + left join sys_dept d on u.dept_id = d.dept_id + left join sys_user_role ur on u.user_id = ur.user_id + left join sys_role r on r.role_id = ur.role_id + + + + + + + + + + + + + + + + + + + + insert into sys_user( + user_id, + dept_id, + user_name, + nick_name, + email, + avatar, + phonenumber, + sex, + password, + status, + pwd_update_date, + create_by, + remark, + create_time + )values( + #{userId}, + #{deptId}, + #{userName}, + #{nickName}, + #{email}, + #{avatar}, + #{phonenumber}, + #{sex}, + #{password}, + #{status}, + #{pwdUpdateDate}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_user + + dept_id = #{deptId}, + nick_name = #{nickName}, + email = #{email}, + phonenumber = #{phonenumber}, + sex = #{sex}, + avatar = #{avatar}, + password = #{password}, + status = #{status}, + login_ip = #{loginIp}, + login_date = #{loginDate}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where user_id = #{userId} + + + + update sys_user set status = #{status} where user_id = #{userId} + + + + update sys_user set avatar = #{avatar} where user_id = #{userId} + + + + update sys_user set pwd_update_date = sysdate(), password = #{password} where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id in + + #{userId} + + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysUserPostMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysUserPostMapper.xml new file mode 100644 index 0000000..9834b7d --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysUserPostMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_user_post where user_id=#{userId} + + + + + + delete from sys_user_post where user_id in + + #{userId} + + + + + insert into sys_user_post(user_id, post_id) values + + (#{item.userId},#{item.postId}) + + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/SysUserRoleMapper.xml b/storm-modules/storm-system/src/main/resources/mapper/system/SysUserRoleMapper.xml new file mode 100644 index 0000000..1c114dd --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/SysUserRoleMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + delete from sys_user_role where user_id=#{userId} + + + + + + delete from sys_user_role where user_id in + + #{userId} + + + + + insert into sys_user_role(user_id, role_id) values + + (#{item.userId},#{item.roleId}) + + + + + delete from sys_user_role where user_id=#{userId} and role_id=#{roleId} + + + + delete from sys_user_role where role_id=#{roleId} and user_id in + + #{userId} + + + \ No newline at end of file diff --git a/storm-modules/storm-system/src/main/resources/mapper/system/bootstrap.yml02 b/storm-modules/storm-system/src/main/resources/mapper/system/bootstrap.yml02 new file mode 100644 index 0000000..54038f0 --- /dev/null +++ b/storm-modules/storm-system/src/main/resources/mapper/system/bootstrap.yml02 @@ -0,0 +1,32 @@ +# Tomcat +server: + port: 9201 + +# Spring +spring: + application: + # 应用名称 + name: storm-system + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 192.168.48.128:8848 + username: nacos + password: nacos + namespace: public + config: + # 配置中心地址 + server-addr: 192.168.48.128:8848 + username: nacos + password: nacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} diff --git a/storm-visual/pom.xml b/storm-visual/pom.xml new file mode 100644 index 0000000..9f9060a --- /dev/null +++ b/storm-visual/pom.xml @@ -0,0 +1,22 @@ + + + + com.storm + storm + 3.6.6 + + 4.0.0 + + + storm-monitor + + + storm-visual + pom + + + storm-visual图形化管理模块 + + + diff --git a/storm-visual/storm-monitor/pom.xml b/storm-visual/storm-monitor/pom.xml new file mode 100644 index 0000000..d37510d --- /dev/null +++ b/storm-visual/storm-monitor/pom.xml @@ -0,0 +1,80 @@ + + + com.storm + storm-visual + 3.6.6 + + 4.0.0 + + storm-visual-monitor + + + storm-visual-monitor监控中心 + + + + + + + de.codecentric + spring-boot-admin-starter-server + ${spring-boot-admin.version} + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.projectlombok + lombok + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/storm-visual/storm-monitor/src/main/java/com/storm/modules/monitor/MonitorApplication.java b/storm-visual/storm-monitor/src/main/java/com/storm/modules/monitor/MonitorApplication.java new file mode 100644 index 0000000..6892b39 --- /dev/null +++ b/storm-visual/storm-monitor/src/main/java/com/storm/modules/monitor/MonitorApplication.java @@ -0,0 +1,44 @@ +package com.storm.modules.monitor; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.env.Environment; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 监控中心 + * + * @author ruoyi + */ +@Slf4j +@EnableAdminServer +@SpringBootApplication +public class MonitorApplication +{ + public static void main(String[] args) throws UnknownHostException { + SpringApplication app = new SpringApplicationBuilder(MonitorApplication.class).build(args); + Environment env = app.run(args).getEnvironment(); + String protocol = "http"; + if (env.getProperty("server.ssl.key-store") != null) { + protocol = "https"; + } + log.info("--/\n---------------------------------------------------------------------------------------\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\t{}://localhost:{}\n\t" + + "External: \t{}://{}:{}\n\t" + + "Profile(s): \t{}" + + "\n---------------------------------------------------------------------------------------", + env.getProperty("spring.application.name"), + protocol, + env.getProperty("server.port"), + protocol, + InetAddress.getLocalHost().getHostAddress(), + env.getProperty("server.port"), + env.getActiveProfiles()); + } +} diff --git a/storm-visual/storm-monitor/src/main/java/com/storm/modules/monitor/config/WebSecurityConfigurer.java b/storm-visual/storm-monitor/src/main/java/com/storm/modules/monitor/config/WebSecurityConfigurer.java new file mode 100644 index 0000000..5897feb --- /dev/null +++ b/storm-visual/storm-monitor/src/main/java/com/storm/modules/monitor/config/WebSecurityConfigurer.java @@ -0,0 +1,51 @@ +package com.storm.modules.monitor.config; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; + +/** + * 监控权限配置 + * + * @author ruoyi + */ +@EnableWebSecurity +public class WebSecurityConfigurer +{ + private final String adminContextPath; + + public WebSecurityConfigurer(AdminServerProperties adminServerProperties) + { + this.adminContextPath = adminServerProperties.getContextPath(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception + { + SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + successHandler.setTargetUrlParameter("redirectTo"); + successHandler.setDefaultTargetUrl(adminContextPath + "/"); + + return httpSecurity + .headers().frameOptions().disable() + .and().authorizeRequests() + .antMatchers(adminContextPath + "/assets/**" + , adminContextPath + "/login" + , adminContextPath + "/actuator/**" + , adminContextPath + "/instances/**" + ).permitAll() + .anyRequest().authenticated() + .and() + .formLogin().loginPage(adminContextPath + "/login") + .successHandler(successHandler).and() + .logout().logoutUrl(adminContextPath + "/logout") + .and() + .httpBasic().and() + .csrf() + .disable() + .build(); + } +} diff --git a/storm-visual/storm-monitor/src/main/resources/banner.txt b/storm-visual/storm-monitor/src/main/resources/banner.txt new file mode 100644 index 0000000..ecaf8a4 --- /dev/null +++ b/storm-visual/storm-monitor/src/main/resources/banner.txt @@ -0,0 +1,10 @@ +Spring Boot Version: ${spring-boot.version} +Spring Application Name: ${spring.application.name} + _ _ _ + (_) (_)| | + _ __ _ _ ___ _ _ _ ______ _ __ ___ ___ _ __ _ | |_ ___ _ __ +| '__|| | | | / _ \ | | | || ||______|| '_ ` _ \ / _ \ | '_ \ | || __| / _ \ | '__| +| | | |_| || (_) || |_| || | | | | | | || (_) || | | || || |_ | (_) || | +|_| \__,_| \___/ \__, ||_| |_| |_| |_| \___/ |_| |_||_| \__| \___/ |_| + __/ | + |___/ \ No newline at end of file diff --git a/storm-visual/storm-monitor/src/main/resources/bootstrap.yml b/storm-visual/storm-monitor/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..ec9766e --- /dev/null +++ b/storm-visual/storm-monitor/src/main/resources/bootstrap.yml @@ -0,0 +1,32 @@ +# Tomcat +server: + port: 9100 + +# Spring +spring: + application: + # 应用名称 + name: storm-monitor + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: storm-nacos:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: storm-nacos:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} diff --git a/storm-visual/storm-monitor/src/main/resources/bootstrap.yml01 b/storm-visual/storm-monitor/src/main/resources/bootstrap.yml01 new file mode 100644 index 0000000..077d153 --- /dev/null +++ b/storm-visual/storm-monitor/src/main/resources/bootstrap.yml01 @@ -0,0 +1,32 @@ +# Tomcat +server: + port: 9100 + +# Spring +spring: + application: + # 应用名称 + name: storm-monitor + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + config: + # 配置中心地址 + server-addr: 192.168.48.129:8848 + username: nacos + password: jsfbnacos + namespace: public + group: DEFAULT_GROUP + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} diff --git a/storm-visual/storm-monitor/src/main/resources/logback.xml b/storm-visual/storm-monitor/src/main/resources/logback.xml new file mode 100644 index 0000000..d2e6196 --- /dev/null +++ b/storm-visual/storm-monitor/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/storm相关.md b/storm相关.md new file mode 100644 index 0000000..15779a3 --- /dev/null +++ b/storm相关.md @@ -0,0 +1,144 @@ +# 部署流程 + +### 安装docker + +``` +# 1、yum 包更新到最新 +yum -y update +# 2、安装需要的软件包,yum-utils提供yum-config-manager功能,另外两个是devicemapper驱动依赖的 +yum install -y yum-utils device-mapper-persistent-data lvm2 --skip-broken +# 3、更新本地镜像源 +yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo +sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo +或 +yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +# 4、 安装docker,出现输入的界面都按 y +yum makecache fast +yum install -y docker-ce +或 +yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin #开启compose功能 +# 5、 查看docker版本,验证是否验证成功 +docker -v + +# 6、其他 + #启动Docker + systemctl start docker + # 停止Docker + systemctl stop docker + # 重启 + systemctl restart docker + # 设置开机自启 + systemctl enable docker + # 执行docker ps命令,如果不报错,说明安装启动成功 + docker ps +``` + +### 安装DockerCompose + +``` +1、方式一:linux通过命令下载: +很慢,直接用方式二 +2、方式二:直接将docker-compose文件上传至linux目录 +具体参考docker-compose文件夹里的readme。 +``` + +### 项目相关 + +打包项目,生成jar,运行docker文件夹下的copy.sh,一键复制jar包,最后docker文件夹上传到服务器里。 + +``` +1、构建镜像 +sudo docker-compose build + +2、启动容器 +启动所有容器 +sudo docker-compose up -d +但是容器启动有顺序要求,具体看deploy.sh +给执行权限 +chmod +x deploy.sh +修改文件格式 Unix 格式 +vim deploy.sh +:set ff=unix +:wq +按顺序执行 +./deploy.sh port +./deploy.sh base +./deploy.sh modules + +3、查看容器启动情况 +docker ps + +4、设置开机自启 +docker update --restart=always 容器名 + +5、更新项目 + + +``` + +# mysql + +### 服务器 + +账号:root + +密码:123456 + +### 本机 + +账号:root + +密码:abc123456 + +# redis + +密码:123456 + +# nacos + +账号:nacos + +密码:jsfbnacos + +# 登录后台 + +使用docker-compose批量部署 + +管理员 + +admin + +admin123 + + + +普通用户 + +user + +654321 + +# 服务器 + +服务器公网地址[113.45.36.253](http://113.45.36.253) + +数据库内网地址[192.168.0.114](http://192.168.0.114) + +账号root + +密码JuShenFengBao1704 + + + +本机虚拟机 + +账号root + +密码123456 + +# 百度地图 + +后端在com.storm.device.service.impl.DeviceServiceImpl替换apiKey + +前端在src\config\map.js里替换 + diff --git a/报错相关命令.md b/报错相关命令.md new file mode 100644 index 0000000..a8f1095 --- /dev/null +++ b/报错相关命令.md @@ -0,0 +1,64 @@ +### 删除旧容器、镜像 + +``` +sudo docker-compose stop storm-nacos + +sudo docker-compose down storm-nacos + +sudo docker image prune -a + +docker images + +sudo docker-compose build storm-nacos + +sudo docker-compose up -d storm-nacos +``` + +### JWT密钥 + +输出 **32** + +``` +openssl rand -base64 32 + +echo -n "wczCOGn5ryZtpt+MdZxv+XxmrER3sYnhE4Mefto8lmc=" | base64 -d | wc -c +``` + + + +### 日志过滤 + +``` +docker logs -f storm-nacos | \ +grep -E 'ERROR|WARN|Caused by|auth|token|JwtTokenManager|nacos.core.auth|secret key|started successfully' + + +# 检查密钥是否被正确加载 +docker logs storm-nacos | grep 'Loading nacos auth secret key' + +# 验证启动阶段鉴权初始化 +docker logs storm-nacos | grep -E 'Init jwt token manager|AuthPluginManager' + +# 确认最终启动状态 +docker logs storm-nacos | grep 'started successfully' + +docker logs storm-nacos | grep -A 50 -B 20 'JwtTokenManager' +``` + +### 验证码字体错误 + +``` +yum install -y fontconfig freetype + +yum install -y libX11 libXext libXrender xorg-x11-fonts-Type1 xorg-x11-fonts-75dpi xorg-x11-fonts-100dpi +``` + + + + + + + +### 其他 + +nacos版本前要加v,这不是装饰,例:v2.3.0, \ No newline at end of file