更新子模块 ruoyi-plus-soybean 的路径和远程仓库地址。

This commit is contained in:
Xuhf 2025-05-23 10:11:42 +08:00
parent eed5a226f9
commit 049934932d
775 changed files with 84371 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
# http://editorconfig.org
root = true
# 空格替代Tab缩进在各种编辑工具下效果一致
[*]
indent_style = space
indent_size = 4
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.{json,yml,yaml}]
indent_size = 2
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

48
.gitignore vendored Normal file
View File

@ -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/
nbdist/
.nb-gradle/
######################################################################
# Others
*.log
*.xml.versionsBackup
*.swp
!*/build/*.java
!*/build/*.html
!*/build/*.xml
.flattened-pom.xml

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.3.1" />
<option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

12
.run/ruoyi-server.run.xml Normal file
View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="ruoyi/ruoyi-server:5.3.1" />
<option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ruoyi-snailjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.3.1" />
<option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "automatic"
}

106
DEPLOYMENT_GUIDE.md Normal file
View File

@ -0,0 +1,106 @@
# RuoYi-Vue-Plus 系统部署指南
## 一、环境准备
1. **JDK**:需要 JDK 17 或 JDK 21
2. **数据库**:支持 MySQL/Oracle/PostgreSQL/SQLServer
3. **Redis**5.0+ 版本
4. **Maven**3.6+ 版本
5. **Node.js**:前端需要(建议 v18+
## 二、后端配置部署
1. **核心配置文件**
- `ruoyi-admin/src/main/resources/application.yml` - 主配置文件
- `application-dev.yml`/`application-prod.yml` - 环境配置
2. **关键配置项**
```yaml
# 数据库配置
spring.datasource:
url: jdbc:mysql://localhost:3306/ry-vue-plus
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# Redis配置
spring.redis:
host: 127.0.0.1
port: 6379
password: ruoyi123
```
3. **数据库初始化**
- 执行对应数据库的SQL脚本位于`script/sql/`目录)
- 例如MySQL执行`mysql_ry_vue_5.X.sql`
4. **启动方式**
必须启动基础建设: mysql redis admin
可选启动基础建设: minio(影响文件上传) monitor(影响监控) snailjob(影响定时任务)
MonitorAdminApplication 为 Admin监控服务(非必要 可参考对应文档关闭 搭建Admin监控)
SnailJobServerApplication 为 任务调度中心服务(非必要 可参考对应文档关闭 搭建调度中心)
DromaraApplication 为 主应用服务
需优先启动 MonitorAdminApplication 与 SnailJobServerApplication 具体配置方式参考对应文档
最后启动 主服务 DromaraApplication
工作流相关初始化使用 工作流初始化
## 三、前端部署
克隆仓库
git clone https://gitee.com/xlsea/ruoyi-plus-soybean.git
cd ruoyi-plus-soybean
安装 pnpm (如果未安装)
npm install pnpm -g
设置淘宝镜像
pnpm config set registry https://registry.npmmirror.com
安装依赖
pnpm install
运行开发服务器
pnpm dev
构建生产版本
pnpm build
## 四、Docker部署可选
1. **使用docker-compose**
```bash
cd script/docker
docker-compose up -d
```
包含的服务:
- Redis
- MinIO
- RuoYi主服务
- 监控中心
2. **独立容器运行**
```bash
docker run -d -p 8080:8080 ruoyi/ruoyi-server:5.3.1
```
## 五、监控中心
1. **监控服务**
- 访问地址:`http://localhost:9090/admin`
- 默认账号ruoyi/123456
2. **任务调度中心**
- 访问地址:`http://localhost:8800`
- 配置见`application-dev.yml`中的`snail-job`部分
## 六、注意事项
1. 首次启动会自动初始化系统数据
2. 默认管理员账号admin/admin123
3. 生产环境建议:
- 修改默认密码
- 关闭Swagger设置`springdoc.swagger-ui.enabled=false`
- 配置HTTPS
完整文档参考:[RuoYi-Vue-Plus文档](https://plus-doc.dromara.org)
ruoyi-vue-plus/
├── ruoyi-admin/ # 后台核心模块
├── ruoyi-common/ # 公共模块库
├── ruoyi-modules/ # 业务模块
├── ruoyi-extend/ # 扩展模块
├── ruoyi-plus-soybean/ # 前端Vue3项目
├── script/ # 部署脚本
├── target/ # 编译输出
├── pom.xml # Maven父工程配置
└── README.md # 项目说明

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2019 RuoYi-Vue-Plus
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.

185
README.md Normal file
View File

@ -0,0 +1,185 @@
<img src="https://foruda.gitee.com/images/1679673773341074847/178e8451_1766278.png" width="50%" height="50%">
<div style="height: 10px; clear: both;"></div>
- - -
## 平台简介
[![码云Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus/badge/star.svg?theme=blue)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![GitHub](https://img.shields.io/github/stars/dromara/RuoYi-Vue-Plus.svg?style=social&label=Stars)](https://github.com/dromara/RuoYi-Vue-Plus)
[![Star](https://gitcode.com/dromara/RuoYi-Vue-Plus/star/badge.svg)](https://gitcode.com/dromara/RuoYi-Vue-Plus)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
[![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
<br>
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.3.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]()
[![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
[![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
> Dromara RuoYi-Vue-Plus 是重写 RuoYi-Vue 针对 `分布式集群与多租户` 场景全方位升级(不兼容原框架)
> 项目代码、文档 均开源免费可商用 遵循开源协议在项目中保留开源协议文件即可<br>
活到老写到老 为兴趣而开源 为学习而开源 为让大家真正可以学到技术而开源
> 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system)
> 官方前端项目地址: [plus-ui](https://gitee.com/JavaLionLi/plus-ui)<br>
> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
> 文档地址: [plus-doc](https://plus-doc.dromara.org)
## 赞助商
MaxKey 业界领先单点登录产品 - https://gitee.com/dromara/MaxKey <br>
CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/ <br>
引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br>
<font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br>
[如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group)
# 本框架与RuoYi的功能差异
| 功能 | 本框架 | RuoYi |
|-------------|-------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| 前端项目 | 采用 Vue3 + TS + ElementPlus 重写 | 基于Vue2/Vue3 + JS |
| 后端项目结构 | 采用插件化 + 扩展包形式 结构解耦 易于扩展 | 模块相互注入耦合严重难以扩展 |
| 后端代码风格 | 严格遵守Alibaba规范与项目统一配置的代码格式化 | 代码书写与常规结构不同阅读障碍大 |
| Web容器 | 采用 Undertow 基于 XNIO 的高性能容器 | 采用 Tomcat |
| 权限认证 | 采用 Sa-Token、Jwt 静态使用功能齐全 低耦合 高扩展 | Spring Security 配置繁琐扩展性极差 |
| 权限注解 | 采用 Sa-Token 支持注解 登录校验、角色校验、权限校验、二级认证校验、HttpBasic校验、忽略校验<br/>角色与权限校验支持多种条件 如 `AND` `OR``权限 OR 角色` 等复杂表达式 | 只支持是否存在匹配 |
| 三方鉴权 | 采用 JustAuth 第三方登录组件 支持微信、钉钉等数十种三方认证 | 无 |
| 关系数据库支持 | 原生支持 MySQL、Oracle、PostgreSQL、SQLServer<br/>可同时使用异构切换(支持其他 mybatis-plus 支持的所有数据库 只需要增加jdbc依赖即可使用 达梦金仓等均有成功案例) | 支持 Mysql、Oracle 不支持同时使用、不支持异构切换 |
| 缓存数据库 | 支持 Redis 5-7 支持大部分新功能特性 如 分布式限流、分布式队列 | Redis 简单 get set 支持 |
| Redis客户端 | 采用 Redisson Redis官方推荐 基于Netty的客户端工具<br/>支持Redis 90%以上的命令 底层优化规避很多不正确的用法 例如: keys被转换为scan<br/>支持单机、哨兵、单主集群、多主集群等模式 | Lettuce + RedisTemplate 支持模式少 工具使用繁琐<br/>连接池采用 common-pool Bug多经常性出问题 |
| 缓存注解 | 采用 Spring-Cache 注解 对其扩展了实现支持了更多功能<br/>例如 过期时间 最大空闲时间 组最大长度等 只需一个注解即可完成数据自动缓存 | 需手动编写Redis代码逻辑 |
| ORM框架 | 采用 Mybatis-Plus 基于对象几乎不用写SQL全java操作 功能强大插件众多<br/>例如多租户插件 分页插件 乐观锁插件等等 | 采用 Mybatis 基于XML需要手写SQL |
| SQL监控 | 采用 p6spy 可输出完整SQL与执行时间监控 | log输出 需手动拼接sql与参数无法快速查看调试问题 |
| 数据分页 | 采用 Mybatis-Plus 分页插件<br/>框架对其进行了扩展 对象化分页对象 支持多种方式传参 支持前端多排序 复杂排序 | 采用 PageHelper 仅支持单查询分页 参数只能从param传 只能单排序 功能扩展性差 体验不好 |
| 数据权限 | 采用 Mybatis-Plus 插件 自行分析拼接SQL 无感式过滤<br/>只需为Mapper设置好注解条件 支持多种自定义 不限于部门角色 | 采用 注解+aop 实现 基于部门角色 生成的sql兼容性差 不支持其他业务扩展<br/>生成sql后需手动拼接到具体业务sql上 对于多个Mapper查询不起作用 |
| 数据脱敏 | 采用 注解 + jackson 序列化期间脱敏 支持不同模块不同的脱敏条件<br/>支持多种策略 如身份证、手机号、地址、邮箱、银行卡等 可自行扩展 | 无 |
| 数据加解密 | 采用 注解 + mybatis 拦截器 对存取数据期间自动加解密<br/>支持多种策略 如BASE64、AES、RSA、SM2、SM4等 | 无 |
| 接口传输加密 | 采用 动态 AES + RSA 加密请求 body 每一次请求秘钥都不同大幅度降低可破解性 | 无 |
| 数据翻译 | 采用 注解 + jackson 序列化期间动态修改数据 数据进行翻译<br/>支持多种模式: `映射翻译` `直接翻译` `其他扩展条件翻译` 接口化两步即可完成自定义扩展 内置多种翻译实现 | 无 |
| 多数据源框架 | 采用 dynamic-datasource 支持市面大部分数据库<br/>通过yml配置即可动态管理异构不同种类的数据库 也可通过前端页面添加数据源<br/>支持spel表达式从请求头参数等条件切换数据源 | 基于 druid 手动编写代码配置数据源 配置繁琐 支持性差 |
| 多数据源事务 | 采用 dynamic-datasource 支持多数据源不同种类的数据库事务回滚 | 不支持 |
| 数据库连接池 | 采用 HikariCP Spring官方内置连接池 配置简单 以性能与稳定性闻名天下 | 采用 druid bug众多 社区维护差 活跃度低 配置众多繁琐性能一般 |
| 数据库主键 | 采用 雪花ID 基于时间戳的 有序增长 唯一ID 再也不用为分库分表 数据合并主键冲突重复而发愁 | 采用 数据库自增ID 支持数据量有限 不支持多数据源主键唯一 |
| WebSocket协议 | 基于 Spring 封装的 WebSocket 协议 扩展了Token鉴权与分布式会话同步 不再只是基于单机的废物 | 无 |
| SSE推送 | 采用 Spring SSE 实现 扩展了Token鉴权与分布式会话同步 | 无 |
| 序列化 | 采用 Jackson Spring官方内置序列化 靠谱!!! | 采用 fastjson bugjson 远近闻名 |
| 分布式幂等 | 参考美团GTIS防重系统简化实现(细节可看文档) | 手动编写注解基于aop实现 |
| 分布式锁 | 采用 Lock4j 底层基于 Redisson | 无 |
| 分布式任务调度 | 采用 SnailJob 天生支持分布式 统一的管理中心 支持多种数据库 支持分片重试DAG任务流等 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造 |
| 文件存储 | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储 | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应 |
| 云存储 | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家 | 不支持 |
| 短信 | 采用 sms4j 短信融合包 支持数十种短信厂家 只需在yml配置好厂家密钥即可使用 可多厂家共用 | 不支持 |
| 邮件 | 采用 mail-api 通用协议支持大部分邮件厂商 | 不支持 |
| 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 |
| 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 |
| Excel框架 | 采用 Alibaba EasyExcel 基于插件化<br/>框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等 | 基于 POI 手写实现 功能有限 复杂 扩展性差 |
| 工作流支持 | 支持各种复杂审批 转办 委派 加减签 会签 或签 票签 等功能 | 无 |
| 工具类框架 | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码 | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等 |
| 监控框架 | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制<br/>实时监控服务状态 框架还为其扩展了在线日志查看监控 | 无 |
| 链路追踪 | 采用 Apache SkyWalking 还在为请求不知道去哪了 到哪出了问题而烦恼吗<br/>用了它即可实时查看请求经过的每一处每一个节点 | 无 |
| 代码生成器 | 只需设计好表结构 一键生成所有crud代码与页面<br/>降低80%的开发量 把精力都投入到业务设计上<br/>框架为其适配MP、SpringDoc规范化代码 同时支持动态多数据源代码生成 | 代码生成原生结构 只支持单数据源生成 |
| 部署方式 | 支持 Docker 编排 一键搭建所有环境 让开发人员从此不再为搭建环境而烦恼 | 原生jar部署 其他环境需手动下载安装 自行搭建 |
| 项目路径修改 | 提供详细的修改方案文档 并为其做了一些改动 非常简单即可修改成自己想要的 | 需要做很多改造 文档说明有限 |
| 国际化 | 基于请求头动态返回不同语种的文本内容 开发难度低 有对应的工具类 支持大部分注解内容国际化 | 只提供基础功能 其他需自行编写扩展 |
| 代码单例测试 | 提供单例测试 使用方式编写方法与maven多环境单测插件 | 只提供基础功能 其他需自行编写扩展 |
| Demo案例 | 提供框架功能的实际使用案例 单独一个模块提供了很多很全 | 无 |
## 本框架与RuoYi的业务差异
| 业务 | 功能说明 | 本框架 | RuoYi |
|--------|----------------------------------------------------------------------|-----|------------------|
| 租户管理 | 系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等 | 支持 | 无 |
| 租户套餐管理 | 系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等 | 支持 | 无 |
| 客户端管理 | 系统内对接的所有客户端管理 如: pc端、小程序端等<br>支持动态授权登录方式 如: 短信登录、密码登录等 支持动态控制token时效 | 支持 | 无 |
| 用户管理 | 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等 | 支持 | 支持 |
| 部门管理 | 配置系统组织机构(公司、部门、小组) 树结构展现支持数据权限 | 支持 | 支持 |
| 岗位管理 | 配置系统用户所属担任职务 | 支持 | 支持 |
| 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等 | 支持 | 支持 |
| 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 | 支持 | 支持 |
| 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 | 支持 | 支持 |
| 参数管理 | 对系统动态配置常用参数 | 支持 | 支持 |
| 通知公告 | 系统通知公告信息发布维护 | 支持 | 支持 |
| 操作日志 | 系统正常操作日志记录和查询 系统异常信息日志记录和查询 | 支持 | 支持 |
| 登录日志 | 系统登录日志记录查询包含登录异常 | 支持 | 支持 |
| 文件管理 | 系统文件展示、上传、下载、删除等管理 | 支持 | 无 |
| 文件配置管理 | 系统文件上传、下载所需要的配置信息动态添加、修改、删除等管理 | 支持 | 无 |
| 在线用户管理 | 已登录系统的在线用户信息监控与强制踢出操作 | 支持 | 支持 |
| 定时任务 | 运行报表、任务管理(添加、修改、删除)、日志管理、执行器管理等 | 支持 | 仅支持任务与日志管理 |
| 代码生成 | 多数据源前后端代码的生成java、html、xml、sql支持CRUD下载 | 支持 | 仅支持单数据源 |
| 系统接口 | 根据业务代码自动生成相关的api接口文档 | 支持 | 支持 |
| 服务监控 | 监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等 | 支持 | 仅支持单机CPU、内存、磁盘监控 |
| 缓存监控 | 对系统的缓存信息查询,命令统计等。 | 支持 | 支持 |
| 在线构建器 | 拖动表单元素生成相应的HTML代码。 | 支持 | 支持 |
| 使用案例 | 系统的一些功能案例 | 支持 | 不支持 |
## 参考文档
使用框架前请仔细阅读文档重点注意事项
<br>
>[初始化项目 必看](https://plus-doc.dromara.org/#/ruoyi-vue-plus/quickstart/init)
>>[https://plus-doc.dromara.org/#/ruoyi-vue-plus/quickstart/init](https://plus-doc.dromara.org/#/ruoyi-vue-plus/quickstart/init)
>
>[专栏与视频 入门必看](https://plus-doc.dromara.org/#/common/column)
>>[https://plus-doc.dromara.org/#/common/column](https://plus-doc.dromara.org/#/common/column)
>
>[部署项目 必看](https://plus-doc.dromara.org/#/ruoyi-vue-plus/quickstart/deploy)
>>[https://plus-doc.dromara.org/#/ruoyi-vue-plus/quickstart/deploy](https://plus-doc.dromara.org/#/ruoyi-vue-plus/quickstart/deploy)
>
>[如何加群](https://plus-doc.dromara.org/#/common/add_group)
>>[https://plus-doc.dromara.org/#/common/add_group](https://plus-doc.dromara.org/#/common/add_group)
>
>[参考文档 Wiki](https://plus-doc.dromara.org)
>>[https://plus-doc.dromara.org](https://plus-doc.dromara.org)
## 软件架构图
![Plus部署架构图](https://foruda.gitee.com/images/1678981882624240692/ae2a3f3e_1766278.png "Plus部署架构图.png")
## 如何参与贡献
[参与贡献的方式 https://plus-doc.dromara.org/#/common/contribution](https://plus-doc.dromara.org/#/common/contribution)
## 捐献作者
作者为兼职做开源,平时还需要工作,如果帮到了您可以请作者吃个盒饭
<img src="https://foruda.gitee.com/images/1678975784848381069/d8661ed9_1766278.png" width="300px" height="450px" />
<img src="https://foruda.gitee.com/images/1678975801230205215/6f96229d_1766278.png" width="300px" height="450px" />
## 演示图例
| | |
|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| ![输入图片说明](https://foruda.gitee.com/images/1680077524361362822/270bb429_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680077619939771291/989bf9b6_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680077681751513929/1c27c5bd_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680077721559267315/74d63e23_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680077765638904515/1b75d4a6_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078026375951297/eded7a4b_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078237104531207/0eb1b6a7_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078254306078709/5931e22f_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078287971528493/0b9af60a_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078308138770249/8d3b6696_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078352553634393/db5ef880_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078378238393374/601e4357_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078414983206024/2aae27c1_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078446738419874/ecce7d59_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078475971341775/149e8634_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078491666717143/3fadece7_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078558863188826/fb8ced2a_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078574561685461/ae68a0b2_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078594932772013/9d8bfec6_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078626493093532/fcfe4ff6_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078643608812515/0295bd4f_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078685196286463/d7612c81_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078703877318597/56fce0bc_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078716586545643/b6dbd68f_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078734103217688/eb1e6aa6_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078759131415480/73c525d8_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078779416197879/75e3ed02_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078802329118061/77e10915_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078893627848351/34a1c342_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078928175016986/f126ec4a_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078941718318363/b68a0f72_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078963175518631/3bb769a1_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1735829153637063344/3c21fd4c_1419627.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1735829181303499815/4522cefa_1419627.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1735829377205259767/76a705d7_1419627.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1722959592856812900/e2d0d342_1419627.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680079274333484664/4dfdc7c0_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079290467458224/d6715fcf_1766278.png "屏幕截图") |

View File

@ -0,0 +1,941 @@
# RuoYi-Vue-Plus 二次开发最佳实践
本文档旨在为基于 RuoYi-Vue-Plus (Soybean Admin Pro 版本前端) 进行二次开发的开发者提供一套实践指南,帮助大家更高效、规范地进行项目开发。
## 1. 引言
### 1.1 文档目的与核心读者
本文档的核心目标是为基于 RuoYi-Vue-Plus (特别是采用 Soybean Admin Pro 前端技术栈的版本) 进行二次开发的开发者提供一套清晰、实用的指南,**专注于如何高效、规范地设计、开发和集成全新的业务模块**。本文档尤其强调如何结合 Cursor 这一现代化 AI 辅助开发工具,进一步提升开发效率和代码质量。
Cursor是一款基于AI的智能编辑器它能够理解您的代码库和开发意图提供高质量的代码补全、重构建议和智能问答能力。使用Cursor进行RuoYi-Vue-Plus项目的二次开发能够显著减少重复性工作提高代码生成效率并保持项目代码风格的一致性。
无论您是需要为现有系统添加新的业务功能,还是希望基于 RuoYi-Vue-Plus 快速构建包含自定义模块的企业级应用,本文档都将力求为您提供从概念到实践的全方位指导,同时充分利用 Cursor 提供的智能代码辅助功能。
### 1.2 RuoYi-Vue-Plus 模块化特性简介
RuoYi-Vue-Plus 在设计上充分考虑了系统的可扩展性和模块化。后端采用多模块 Maven 项目结构,前端也支持模块化的视图和路由管理。这种设计使得开发者可以相对独立地开发和维护各个业务模块,降低了模块间的耦合度,提高了开发效率和系统的可维护性。
"模块"通常指一组相关联的业务功能单元,例如"订单管理模块"、"产品管理模块"或"客户关系模块",它们在后端有独立的 Maven 模块(通常在 `ruoyi-modules`在前端有对应的视图组件、API 服务和路由配置。
通过使用 Cursor 进行开发您可以借助其强大的代码智能补全和上下文理解能力使模块化开发更加高效。Cursor能够
- 分析现有代码结构,快速理解项目架构和组件关系
- 根据上下文提供符合项目规范的代码片段
- 协助生成前后端一致的API接口和数据模型
- 智能重构和改进现有代码,保持风格一致性
- 减少常见错误和提高代码质量
### 1.3 二次开发新模块的核心原则
在进行新模块开发时,我们遵循以下核心原则:
* **高内聚、低耦合**:新模块内部功能紧密相关,模块之间尽可能减少直接依赖。
* **遵循框架规范**:充分利用并遵循 RuoYi-Vue-Plus 已有的架构设计、代码规范和工具。
* **可复用性**:考虑模块内部组件和服务的可复用性,避免代码冗余。
* **可维护性**:编写清晰、易懂、易于测试的代码,确保长期维护成本最小化。
* **安全性**:从设计之初就考虑新模块的权限控制和数据安全,防范常见的安全风险。
* **AI辅助开发**:合理利用 Cursor 提供的 AI 能力进行代码生成、重构和优化,同时保持对生成代码的质量审核。
### 1.4 使用Cursor提升开发体验
作为一款智能开发工具Cursor在RuoYi-Vue-Plus二次开发中可以提供以下关键优势
- **快速上手项目**:使用聊天功能直接询问项目结构、核心组件和开发规范
- **智能代码生成**:基于项目上下文自动生成符合规范的代码片段和完整模块
- **跨语言开发支持**同时支持后端Java和前端Vue/TypeScript的智能补全和建议
- **代码解释和重构**:为复杂代码提供解释,并建议更优的实现方案
- **标准化编码**:帮助团队保持一致的代码风格和最佳实践
- **学习辅助**:通过提问了解框架各组件的用法,减少学习曲线
要充分利用Cursor的能力建议
1. 将整个项目代码库导入Cursor工作区
2. 在使用AI功能前确保已浏览关键部分的代码以建立上下文
3. 使用具体、明确的提示来获取最佳结果
4. 先让Cursor解释代码后再请求修改或生成新代码
## 2. 环境准备与项目结构
在开始新模块开发之前,请确保您的开发环境已正确配置,并熟悉项目的基本结构。
### 2.1 开发环境要求
* **后端**:
* JDK 17+ (请根据项目 `pom.xml` 中指定的 Java 版本为准)
* Maven 3.6+
* Redis 6.0+
* MySQL 8.0+ (或其他兼容数据库如PostgreSQL, Oracle)
* IDE: IntelliJ IDEA (推荐,对 Maven 和 Spring Boot 支持良好)
* **前端** (`ruoyi-plus-soybean`):
* Node.js 16.x 或 18.x+
* 包管理器: pnpm 8.x+ (强烈推荐,项目默认配置)
* IDE: VSCode 配合 Cursor 插件 (推荐) 或 Cursor 独立应用
* **AI辅助开发工具**:
* Cursor IDE (集成了 Claude AI 的智能编辑器)
* 配置建议:
* 启用代码补全和聊天功能
* 设置项目级别的上下文提示,说明项目结构和模块关系
* 配置保存检查确保生成的代码符合项目linting规则
* 设置快捷键加速常用AI操作的执行
* 对于团队协作,考虑共享提示模板和命令
*重要提示*:
1. 确保所有环境与 RuoYi-Vue-Plus 主项目所依赖的版本兼容。建议查阅项目根目录的 `pom.xml` (后端) 和 `package.json` (前端) 以获取精确的版本信息。
2. 使用 Cursor 进行开发时,可以通过命令面板 (Cmd/Ctrl+Shift+P) 选择 "Chat with codebase" 功能,帮助快速理解项目结构和代码逻辑。
3. 建议添加对项目特定文件如README.md、架构图文档、API规范的上下文帮助Cursor更好地理解项目规范。
### 2.2 后端项目结构 (`ruoyi-vue-plus`)
后端项目采用典型的 Maven 多模块结构,这对于添加新业务模块至关重要:
* `ruoyi-vue-plus` (根项目)
* `pom.xml`: 主 POM 文件,管理所有模块的依赖和插件。**新增业务模块后,需要在此处的 `<modules>` 标签内注册新模块。**
* `ruoyi-admin`: 启动和核心管理模块。
* `ruoyi-common`: 通用模块父级,包含多个功能明确的子模块:
* `ruoyi-common-core`: 核心工具类、常量、枚举、通用 Service/Mapper 接口等。
* `ruoyi-common-mybatis`: MyBatis Plus 相关配置和基类。
* `ruoyi-common-redis`: Redis 相关配置和工具。
* `ruoyi-common-satoken`: SaToken 权限认证相关。
* `ruoyi-common-log`: 操作日志和系统日志处理。
* `ruoyi-common-web`: Web 相关通用处理如全局异常、XSS过滤等。
* `ruoyi-common-excel`: Excel导入导出处理。
* `ruoyi-common-translation`: 数据翻译功能。
* `ruoyi-common-tenant`: 多租户支持。
* ... (其他通用组件)
* `ruoyi-modules`: **所有业务模块的父级模块。您开发的新业务模块将作为其子模块存放。**
* `ruoyi-system`: 系统管理模块 (用户、角色、菜单、部门、字典等)。
* `ruoyi-generator`: 代码生成器模块。
* `ruoyi-job`: 定时任务模块。
* `ruoyi-workflow`: 工作流模块(如果已启用)。
* `your-new-module`: **您新增的业务模块将放置于此,例如 `ruoyi-pms` (商品管理)。**
* `ruoyi-extend`: 扩展模块,如监控等。
**Cursor使用技巧**
- 使用"文件探索"功能快速导航到关键模块的代码文件
- 通过聊天功能询问"系统模块之间的依赖关系是什么?"来理解项目架构
- 请求Cursor生成新模块的Maven配置文件如"帮我创建一个名为ruoyi-crm的新业务模块的pom.xml文件"
- 让Cursor分析现有模块的结构如"分析ruoyi-system模块的目录结构和主要类"
- 使用Cursor生成符合项目规范的新模块基础结构
### 2.3 前端项目结构 (`ruoyi-plus-soybean`)
前端项目基于 Soybean Admin Pro采用 Vue 3 + TypeScript + Naive UI 技术栈,组织结构清晰:
* `ruoyi-plus-soybean` (项目根目录)
* `src/`: 主要源代码目录。
* `views/`: 页面视图组件。**新模块的页面通常会在此目录下创建对应子目录**,例如 `src/views/pms/product/index.vue`
* `service/api/`: API 请求服务。**新模块的后端接口调用会在此目录下创建对应的 `ts` 文件**,例如 `src/service/api/pms/product.ts`
* `router/routes/modules/`: 路由配置文件。**新模块的路由信息会在此目录下创建新的 `ts` 文件**,如 `product.ts`
* `store/modules/`: Pinia 状态管理模块。如果新模块有复杂的状态管理需求,可以在此创建。
* `locales/langs/`: 国际化语言包。新模块中需要国际化的文本,其键值对会添加到此处的语言文件中。
* `components/`: 通用或业务组件。
* `common/`: 通用基础组件。
* `custom/`: 业务相关组件。
* `advanced/`: 复杂功能组件。
* `hooks/`: 自定义 Hooks (Composition API)。
* `business/`: 业务相关的钩子函数,如表格操作、权限校验等。
* `common/`: 通用钩子函数如状态管理、UI交互等。
* `typings/`: TypeScript 类型定义,特别是 API 相关的类型。
* `api/`: 后端接口类型定义。
* `utils/`: 工具函数集合。
**Cursor使用技巧**
- 使用聊天功能分析前端项目结构:"分析ruoyi-plus-soybean前端项目的文件组织和主要模块"
- 了解组件使用模式:"ruoyi-plus-soybean项目中的表格组件是如何封装的?"
- 根据已有模块生成新模块:"基于system模块的用户管理页面创建一个产品管理页面的基础结构"
- 分析Hooks使用方式"分析项目中useTable这个hook的用法和参数"
- 生成符合规范的API服务"为产品管理模块创建API服务文件包含列表查询、新增、修改和删除方法"
### 2.4 代码生成器简介与Cursor协同使用
RuoYi-Vue-Plus 提供了强大的代码生成器功能(通常在系统后台的"开发工具"→"代码生成"菜单下)。它可以根据数据库表结构自动生成包括后端 Controller, Service, Mapper, Domain (Entity, BO, VO) 以及前端 Vue 页面和 API 调用代码。
代码生成步骤:
1. 配置数据源(如果尚未配置)
2. 导入要生成代码的数据表
3. 编辑表和字段配置(包括字段显示类型、查询方式、是否必填等)
4. 生成代码
5. 下载代码包或直接生成到项目中
**强烈建议在创建新模块的基础 CRUD 功能时,优先使用代码生成器**,它可以显著提高开发效率并保证代码结构的统一性。后续可在生成代码的基础上进行业务逻辑的定制和优化。
**代码生成器与Cursor协同工作**:
1. 使用代码生成器生成基础CRUD代码
2. 将生成的代码导入项目
3. 使用Cursor分析生成的代码结构"分析这个新生成的XXX模块的代码结构和主要功能点"
4. 让Cursor帮助优化生成的代码"优化这个Controller中的分页查询方法添加更多的查询条件支持"
5. 使用Cursor扩展业务逻辑"在这个Service中添加一个批量处理的方法"
6. 请求Cursor解释复杂部分"解释这段MyBatis XML中的动态SQL语句的功能"
这种组合利用代码生成器的快速创建能力和Cursor的智能优化能力可以最大化提升开发效率同时确保代码质量和一致性。
## 3. 模块深入分析:以"用户管理"为例
本章通过对用户管理模块的分析,帮助理解 RuoYi-Vue-Plus 项目的模块设计、分层架构以及常用功能的集成方式。
### 3.1 使用Cursor理解模块架构
在开始开发新模块前彻底理解项目的现有模块结构和设计模式至关重要。Cursor能够帮助开发者快速分析和理解复杂的模块架构特别是像用户管理这样的核心功能模块。
以下是使用Cursor理解用户管理模块的有效方式
1. **快速概览模块结构**
* 使用聊天功能询问:"用户管理模块的主要组件和类有哪些?"
* 让Cursor分析关键类之间的关系"分析SysUserController、SysUserService和SysUserMapper之间的交互流程"
2. **分析数据流转**
* 请求Cursor绘制数据流程"从前端请求到后端处理,用户管理模块的数据流是怎样的?"
* 了解BO/VO对象转换"SysUserBo和SysUserVo的区别和转换过程是什么"
3. **分析权限控制**
* 理解注解应用:"分析用户管理模块中@SaCheckPermission的使用场景和实现逻辑"
* 探索数据权限:"用户管理模块如何实现基于部门的数据权限?"
4. **理解业务逻辑**
* 让Cursor解释复杂方法"解释SysUserServiceImpl中的importUser方法的业务逻辑"
* 分析关键业务规则:"用户管理模块中的密码策略是如何实现的?"
通过这些提问和分析,可以快速掌握模块的设计理念和实现细节,为开发新模块打下坚实基础。
### 3.2 后端实现 (`ruoyi-system` 模块)
后端实现遵循经典的分层架构:
* **Controller 层**: 负责接收前端请求,进行初步参数校验和权限验证,然后调用 Service 层处理业务逻辑,并返回统一格式的响应。它主要关注 HTTP 请求和响应的处理。
* **Service 层**: 包含核心业务逻辑。负责协调对 Mapper 层的调用执行复杂的业务规则处理事务和缓存等。Service 层是业务流程的核心。
* **Mapper 层**: 负责与数据库进行直接交互,执行 SQL 语句完成数据的持久化和检索。通常使用 MyBatis Plus。
* **Domain 层**: 定义数据对象,包括:
* **Entity**: 映射数据库表结构,用于 ORM 操作。
* **BO (Business Object)**: 用于 Controller 层接收请求参数和 Service 层内部数据传递,主要负责参数校验。
* **VO (View Object)**: 用于 Service 层向 Controller 层返回数据,最终响应给前端,包含前端展示所需的字段,可能进行数据脱敏和翻译。
**Cursor使用提示** 可以向Cursor请求"生成一个分析用户管理模块各层次依赖关系的结构图",快速理解分层架构。
#### 3.2.1 核心功能实现分析
用户管理模块中的核心功能在后端各层的应用:
* **权限控制 (SaToken)**: 通过注解或编程式方式在 Controller 层对接口进行访问权限校验。
* **Cursor分析点** 请求"分析SaToken在用户管理模块中的应用方式和关键代码"
* **数据权限**: 在 Mapper 层通过注解实现,根据用户角色动态过滤数据访问范围。
* **Cursor分析点** 询问"解释@DataPermission注解在SysUserMapper中的具体作用"
* **操作日志**: 通过注解在 Controller 层记录用户的业务操作。
* **Cursor分析点** 请求"展示用户管理模块中操作日志注解的使用示例"
* **参数校验**: 主要在 BO 对象上通过 JSR 303/380 注解实现Controller 层触发校验。
* **Cursor分析点** 询问"用户管理模块使用了哪些校验注解及其作用?"
* **Excel 导入/导出**: 利用 EasyExcel 工具类及其注解简化操作。
* **Cursor分析点** 请求"分析用户Excel导入导出功能的核心实现逻辑"
* **多租户处理**: 通过框架提供的机制在 Entity 和 Mapper 层实现数据隔离。
* **Cursor分析点** 询问"用户管理模块如何处理多租户数据隔离?"
* **事务管理**: 在 Service 层通过 `@Transactional` 注解确保数据库操作的原子性。
* **Cursor分析点** 请求"找出用户管理模块中的事务应用示例"
* **缓存使用 (Redis)**: 通过 Spring Cache 注解和工具类提升数据访问性能。
* **Cursor分析点** 询问"用户管理模块的哪些数据使用了缓存,如何实现?"
* **国际化**: 通过资源文件和 MessageSource 在后端实现多语言支持。
* **Cursor分析点** 请求"显示用户管理模块中国际化的实现方式"
* **统一结果与异常处理**: 使用统一的响应结构和全局异常处理器提升代码一致性和用户体验。
* **Cursor分析点** 询问"用户管理模块如何处理业务异常并返回统一结果?"
通过Cursor对这些核心功能的分析可以深入理解框架的设计理念和实现方式为开发新模块奠定基础。
### 3.3 前端实现 (`ruoyi-plus-soybean`)
前端实现主要基于 Vue 3、TypeScript 和 Soybean Admin Pro 框架:
* **视图组件**: 使用 Vue 3 组件构建页面,通常包含搜索区、操作按钮区、数据表格等。
* **Hooks 应用**: 大量使用框架提供的自定义 Hooks (Composables) 封装和复用通用逻辑,如表格数据处理、操作处理、权限校验、字典获取等。
* **API 服务**: 集中封装所有与后端交互的 API 请求函数,提供统一的请求处理和类型约束。
* **路由配置**: 定义页面访问路径、组件加载和菜单元信息,通常按模块组织。
**Cursor使用提示** 可以请求Cursor"分析用户管理前端组件的结构和主要功能块",快速理解前端实现。
#### 3.3.1 前端功能实现分析
前端用户管理模块的核心功能实现:
* **接口调用与数据处理**: 调用封装的 API 服务函数,处理后端数据更新视图。
* **Cursor分析点** 请求"分析用户管理模块的API调用和数据处理流程"
* **表单与校验**: 使用 UI 组件库构建表单,配合校验规则进行数据验证。
* **Cursor分析点** 询问"用户表单组件的校验规则是如何设置的?"
* **权限控制**: 利用 `useAuth` Hook 判断用户权限,控制页面元素的显示和操作。
* **Cursor分析点** 请求"展示useAuth在用户管理页面中的应用案例"
* **国际化**: 使用 `$t` 函数实现界面文本的多语言支持。
* **Cursor分析点** 请求"显示用户管理模块中国际化的实现方式"
* **状态管理 (Pinia)**: 管理全局或跨组件共享的状态,如用户认证信息、数据字典等。
* **Cursor分析点** 请求"分析用户管理使用了哪些Pinia状态管理"
* **子组件通信**: 通过标准的 Props 和 Emits 实现组件间的数据传递和事件交互。
* **Cursor分析点** 询问"用户管理页面中的组件通信方式有哪些?"
通过对前端实现的深入分析结合Cursor的辅助可以清晰理解Vue 3 + TypeScript的组件化开发模式和最佳实践为新模块的前端开发提供参考。
### 3.4 使用Cursor复现模块架构
通过对用户管理模块的分析我们可以使用Cursor帮助复现类似的架构到新的业务模块中
1. **基础结构生成**:
* 请求"根据用户管理模块的结构,生成一个产品管理模块的基础结构框架"
* 让Cursor生成新模块所需的关键文件列表
2. **分层设计复现**:
* 请求"参考用户管理模块设计产品管理模块的Entity, BO, VO对象结构"
* 让Cursor生成"产品管理模块的Controller、Service、Mapper接口定义"
3. **功能特性实现**:
* 请求"基于用户管理模块,为产品管理实现数据权限控制代码"
* 让Cursor生成"产品管理的Excel导入导出功能实现代码"
4. **前端组件设计**:
* 请求"参考用户管理的前端组件,设计产品管理的页面组件结构"
* 让Cursor生成"产品管理的API服务和类型定义文件"
通过这种方式Cursor能够在理解现有模块架构的基础上生成高度符合项目规范的新模块代码大幅提高开发效率并保证代码一致性。
## 4. 后端开发最佳实践
本章节将基于对RuoYi-Vue-Plus特别是用户管理模块的分析并结合通用的后端开发原则和Cursor辅助开发能力总结在进行二次开发时推荐遵循的最佳实践。
### 4.1 Cursor辅助后端开发技巧
在进行RuoYi-Vue-Plus的后端开发时Cursor可以显著提升开发效率和代码质量。以下是一些关键的辅助开发技巧
1. **快速理解项目结构**
* 使用Cursor分析整体架构"分析RuoYi-Vue-Plus后端的核心模块结构和依赖关系"
* 了解设计模式:"RuoYi-Vue-Plus中应用了哪些设计模式请举例说明"
* 生成模块结构图:"绘制一个RuoYi-Vue-Plus模块依赖关系图"
2. **代码生成与完善**
* 生成常用类文件:"基于RuoYi规范创建一个ProductController类"
* 补充接口方法:"为ProductService接口添加批量导入和导出方法"
* 实现复杂逻辑:"实现一个根据销售状态统计产品数量的Service方法"
* 增强生成代码:"优化代码生成器生成的ProductServiceImpl类添加缓存支持"
3. **分析与解释现有代码**
* 理解原理:"解释SaToken在RuoYi-Vue-Plus中的集成方式和工作原理"
* 分析性能:"分析UserServiceImpl中这个查询方法的性能瓶颈"
* 学习最佳实践:"RuoYi项目中MyBatis动态SQL的最佳实践有哪些"
4. **代码重构与优化**
* 优化复杂方法:"重构这个导入用户的方法,提高可读性和维护性"
* 消除代码气味:"这段代码有什么可以改进的地方?"
* 提取公共逻辑:"从这几个Service中提取通用的验证逻辑到工具类"
5. **排查与修复问题**
* 分析错误:"为什么这个查询方法返回的结果和预期不符?"
* 性能诊断:"为什么这个接口响应很慢?可能的原因和解决方案"
* 解决冲突:"解决这个多租户环境下的数据访问冲突问题"
通过这些技巧开发者可以充分利用Cursor的AI能力加快开发进度提升代码质量同时深入理解项目架构和设计理念。
### 4.2 模块设计与创建
* **单一职责原则 (SRP)**: 设计模块时,应确保每个模块聚焦于一块明确、独立的业务功能领域(例如,系统管理模块 `ruoyi-system` 负责用户、角色、菜单、部门、字典等;产品管理模块 `ruoyi-pms` 负责产品信息的维护)。避免创建过于庞大、职责不清的"上帝模块",这会导致业务逻辑交织,难以理解、维护和独立升级。
**使用Cursor进行模块设计**
* 分析现有模块分工:"分析RuoYi-Vue-Plus各业务模块的职责边界和交互方式"
* 设计新模块:"帮我设计一个订单管理模块的功能和边界,考虑与其他模块的关系"
* 避免过度耦合:"检查我设计的这个模块是否存在责任划分不清的问题"
* **模块命名规范**:
* **Maven模块名**: 采用小写字母多个单词之间使用中横线连接kebab-case`ruoyi-order-management`, `ruoyi-customer-support`。这与RuoYi-Vue-Plus现有的模块命名风格`ruoyi-common-core`)保持一致。
* **Java包名**: 遵循标准的Java包命名约定通常以反向域名开头后跟项目名和模块名全小写。例如`org.dromara.ordermanagement` 或 `com.yourcompany.yourproject.customersupport`
* **模块依赖管理**:
* **清晰的依赖关系**: 业务模块(如 `ruoyi-pms`)应明确依赖其所需的平台公共模块(如 `ruoyi-common-core`, `ruoyi-common-mybatis`, `ruoyi-common-web`, `ruoyi-common-satoken`, `ruoyi-common-excel` 等)。
* **避免业务模块间的直接强依赖**: 如果业务模块A需要调用业务模块B的功能应优先考虑是否可以通过更松耦合的方式实现例如
* **API调用**: 如果模块未来可能独立部署为微服务或者希望保持高度的独立性可以设计模块间的API接口通过HTTP/RPC调用如使用OpenFeign若项目引入了Spring Cloud相关技术栈
* **异步消息/事件**: 对于非核心业务流程的解耦、最终一致性场景或耗时操作推荐使用消息队列如RocketMQ、Kafka需额外集成或Spring框架内置的事件发布/监听机制 (`ApplicationEvent`)。
* **共享公共服务/DTO**: 如果仅仅是共享一些通用的业务逻辑或数据结构,可以考虑将其抽象并下沉到 `ruoyi-common-core` 或创建一个新的、更细粒度的 `ruoyi-common-{feature}` 模块中。
* **依赖的最小化原则**: 只依赖真正需要的模块和库,避免不必要的依赖传递和潜在的冲突。
* **新模块创建流程** (回顾PMS模块示例):
1. 在 `ruoyi-vue-plus/ruoyi-modules` 父Maven模块下通过IDE或Maven命令创建新的子模块。
2. 在新模块的 `pom.xml` 文件中,将其 `<parent>` 指向 `ruoyi-modules`并根据新模块的业务需求添加对RuoYi公共模块`ruoyi-common-core`, `ruoyi-common-mybatis`等)和其他第三方库的依赖。
3. 在项目根目录的 `pom.xml``ruoyi-modules/pom.xml` 文件的 `<modules>` 部分,添加新创建的模块名。
4. 在新模块的 `src/main/java/` 目录下,按照既定的包结构(如 `org.dromara.{模块名}`)创建 `controller`, `service`, `mapper`, `domain` (包含 `entity`, `bo`, `vo`) 等子包。
5. 在 `src/main/resources/` 目录下,创建 `mapper/{模块名}` 子目录用于存放MyBatis的XML映射文件以及 `i18n` 目录用于存放国际化属性文件 (如 `messages_zh_CN.properties`, `messages_en_US.properties`)。
### 4.3 分层架构约定
RuoYi-Vue-Plus 遵循了经典且职责清晰的分层架构,二次开发时应严格遵守此约定:
* **Controller (表现层 / 控制层)**:
* **核心职责**: 作为HTTP请求的直接入口负责请求的接收、参数的初步绑定与校验主要通过JSR 303/380注解在BO上完成、调用相应的Service层方法来执行核心业务逻辑、组装Service层返回的数据并构建统一的响应结果 (`R` 对象) 返回给前端。
* **关注点**: HTTP协议相关的处理如路径映射 (`@RequestMapping`, `@GetMapping`等)、请求参数解析 (`@PathVariable`, `@RequestParam`, `@RequestBody`等)、权限控制 (`@SaCheckPermission`)、操作日志记录 (`@Log`)。
* **禁止**: 直接调用Mapper层进行数据库操作包含复杂的业务流程代码直接处理数据库实体 `Entity` 的输入输出应使用BO和VO
* **Service (业务逻辑层)**:
* **接口 (`ISyourService.java`)**: 定义业务操作的契约,明确本业务模块能提供的服务能力。面向接口编程是推荐的。
* **实现 (`YourServiceImpl.java`)**: 包含核心的业务逻辑实现。负责编排对一个或多个Mapper方法的调用、组合其他Service提供的服务、执行复杂的业务规则校验、管理事务边界、处理缓存逻辑读/写)、发送领域事件或消息等。
* **禁止**: 直接依赖或处理HTTP相关的对象 (如 `HttpServletRequest`, `HttpServletResponse`);将数据库实体 `Entity` 直接返回给Controller层应转换为VO
* **事务管理**: 在实现类的公开业务方法上使用 `@Transactional(rollbackFor = Exception.class)` 注解来声明事务边界,确保数据操作的原子性和一致性。
* **Mapper (数据访问层)**:
* **接口 (`YourMapper.java`)**: 继承自框架提供的 `BaseMapperPlus<Entity, Vo>`并可以定义针对特定业务需求的自定义数据库操作方法。对于复杂的SQL语句应在对应的XML映射文件中编写。
* **核心职责**: 负责与数据库进行直接交互执行SQL语句完成数据的持久化增删改和检索
* **禁止**: 包含任何业务逻辑代码被Controller层直接调用。
* **数据权限**: 在Mapper接口方法上使用框架提供的 `@DataPermission` 注解来实现声明式的数据权限控制。
* **Domain (领域对象层)**:
* **Entity (`YourEntity.java`)**: 数据库表在Java中的映射对象用于ORM操作其字段和结构应与数据表严格对应。
* **BO (`YourBo.java`)**: Business Object (业务对象)也常作为DTO (Data Transfer Object)。主要用于Controller层接收前端传入的参数以及在Service层内部进行数据传递。BO对象是进行参数校验 (JSR 303/380注解) 的主要场所。
* **VO (`YourVo.java`)**: View Object (视图对象)。主要用于Service层向Controller层返回处理结果最终序列化为JSON响应给前端。VO的字段应为前端展示所需并可能包含经过数据脱敏 (`@Sensitive`)、数据翻译 (`@Translation`) 或其他格式化处理的数据。
**遵循分层架构的好处**:
* **高内聚、低耦合**: 各层职责单一且明确,层与层之间通过接口或定义好的数据对象进行交互,降低了代码的耦合度。
* **可维护性**: 清晰的结构使得代码更易于理解、修改和维护。问题定位也更加方便。
* **可测试性**: 可以针对每一层尤其是Service层和Mapper层独立编写单元测试和集成测试。
* **可扩展性与复用性**: 更容易在不影响其他层的情况下替换某一层或某一部分的具体实现,业务逻辑也更容易被复用。
### 4.4 Domain 对象规范 (Entity, BO, VO)
在RuoYi-Vue-Plus中合理设计和使用Entity、BO、VO对于保持代码清晰和高效至关重要。
* **Entity (`YourEntity.java`) - 持久化对象**: (如 `SysUser`)
* **用途**: 直接映射数据库表作为MyBatis Plus进行ORM操作的基础。
* **规范**:
* 使用 `@TableName` 指明对应的数据库表名。
* 使用 `@TableId` 标记主键字段,并可指定主键策略 (如雪花算法 `IdType.ASSIGN_ID`)。
* 对于逻辑删除字段,使用 `@TableLogic` 注解。
* 字段命名采用驼峰式对应数据库表的下划线命名MyBatis Plus默认支持此转换
* 继承框架提供的基类 (如 `TenantEntity` 以支持多租户和通用审计字段,或 `BaseEntity` 仅包含审计字段)。
* **不应**包含任何业务逻辑方法 (除了简单的、基于自身属性的判断,如 `SysUser::isSuperAdmin`)。
* **不应**包含与数据库持久化无关的临时字段或计算字段。
* **BO (`YourBo.java`) - 业务/数据传输对象**: (如 `SysUserBo`)
* **用途**:
1. Controller层接收前端HTTP请求体 (`@RequestBody`) 或查询参数时的数据封装对象。
2. Service层方法的形式参数类型用于传递业务操作所需的数据。
3. Service层内部复杂业务逻辑处理时的数据载体。
* **规范**:
* 字段可以部分来源于Entity也可以包含Entity中没有的、特定于业务操作的辅助字段例如`SysUserBo` 中的 `roleIds`, `postIds` 用于关联操作;`params` Map中的 `beginTime`, `endTime` 用于范围查询)。
* **必须**承担参数校验的主要职责在其字段上使用JSR 303/380标准校验注解 (`@NotBlank`, `@NotNull`, `@Size`, `@Min`, `@Max`, `@Email`, `@Pattern`等) 以及框架自定义的校验注解 (如 `@Xss`)。
* 可以使用MapStruct Plus的 `@AutoMapper(target = YourEntity.class)` 注解来辅助与Entity对象之间的属性复制简化转换代码。
* **不应**直接用于数据库的持久化操作应先转换为对应的Entity对象
* **不应**包含大量与前端展示强相关的逻辑或格式化字段这些应在VO中处理
* **VO (`YourVo.java`) - 视图对象**: (如 `SysUserVo`, `SysUserExportVo`, `SysUserImportVo`)
* **用途**: Service层处理完业务逻辑后将结果数据封装成VO对象返回给Controller层最终序列化为JSON响应给前端。
* **规范**:
* 字段内容应完全根据前端界面的展示需求来定制。可能只包含Entity中的部分字段也可能包含通过关联查询、计算或数据翻译得到的额外字段。
* 对于敏感信息字段(如用户邮箱、手机号),应使用框架提供的 `@Sensitive` 注解进行数据脱敏处理,并可根据权限 (`perms`)决定是否对特定用户展示完整信息。
* 对于需要代码/ID转换的字段如部门ID转部门名称、用户ID转用户昵称、字典值转字典标签、OSS文件ID转URL应使用框架提供的 `@Translation` 注解进行自动翻译。
* 对于导出Excel的场景应创建专门的ExportVo (如 `SysUserExportVo`),其字段使用 `@ExcelProperty` 注解定义Excel列标题并使用 `@ExcelDictFormat` 等注解处理字典转换。
* 对于从Excel导入数据的场景应创建专门的ImportVo (如 `SysUserImportVo`),其字段同样使用 `@ExcelProperty` 匹配Excel列并可使用 `@ExcelDictFormat` 进行字典值的反向转换。
* **绝不应**包含用户的密码等高度敏感且不应在前端展示的原始信息。
* 可以使用MapStruct Plus的 `@AutoMapper(target = YourEntity.class)` 注解通常是反向的从Entity/BO生成VO来辅助对象属性的复制。
* **不应**用于接收前端的输入参数这是BO的职责
* **不应**包含业务校验逻辑。
* **命名约定**:
* Entity: 直接使用领域名词,如 `Product`, `Order`
* BO: 通常以 `...Bo` 为后缀,如 `ProductBo`, `OrderQueryBo`
* VO: 通常以 `...Vo` 为后缀,如 `ProductVo`, `OrderDetailVo`。特定用途的VO可以更具体`ProductExportVo`
* **对象转换**:
* 推荐使用MapStruct Plus (`@AutoMapper`) 或其他成熟的Java Bean映射工具 (如 `cn.hutool.core.bean.BeanUtil.copyProperties`) 来完成Entity, BO, VO之间的属性复制以减少手动编写大量getter/setter的模板代码并提高代码的可读性和可维护性。
* 进行对象转换时需注意深拷贝与浅拷贝的问题特别是当对象中包含集合或复杂嵌套对象时。对于复杂映射逻辑MapStruct Plus也支持自定义映射方法。
* **序列化**: 所有VO对象以及可能需要缓存的Entity/BO对象都应实现 `java.io.Serializable` 接口这是Java序列化的基本要求对于对象的缓存、分布式Session共享、RPC传输等场景非常重要。
## 5. 前端开发最佳实践
基于对RuoYi-Vue-Plus (Soybean Admin Pro 版本前端) 的分析,本章节将提供前端开发的最佳实践指南,帮助开发人员高效地进行二次开发。
### 5.1 Cursor辅助前端开发技巧
在进行RuoYi-Vue-Plus的前端开发时Cursor可以显著提升开发效率和代码质量。以下是一些关键的辅助开发技巧
1. **快速理解项目结构**
* 分析项目架构:"分析ruoyi-plus-soybean前端项目的整体架构和核心概念"
* 查看技术栈:"列出ruoyi-plus-soybean使用的主要技术栈和库"
* 生成目录结构图:"绘制一个ruoyi-plus-soybean前端项目的目录结构图"
2. **组件开发与完善**
* 生成业务组件:"基于项目规范创建一个产品详情页组件"
* 添加表单验证:"为这个产品表单添加完整的字段验证规则"
* 实现复杂UI"实现一个带有拖拽排序功能的产品分类管理组件"
* 增强现有组件:"为这个表格组件添加导出Excel功能"
3. **前端工具类分析**
* 理解工具实现:"解释useTable hook的实现原理和主要功能"
* 分析性能:"分析这个表单渲染方法的性能瓶颈"
* 学习最佳实践:"项目中API请求封装的最佳实践有哪些"
4. **代码重构与优化**
* 优化复杂组件:"重构这个多级表单组件,提高可维护性"
* 消除代码气味:"这段TypeScript代码有哪些可以改进的地方"
* 提取公共逻辑:"从这几个组件中提取通用的表单处理逻辑到Hook"
5. **排查与修复前端问题**
* 分析渲染问题:"为什么这个组件首次加载时显示异常?"
* 性能诊断:"为什么这个表格组件滚动卡顿?可能的原因和解决方案"
* 解决兼容性问题:"解决这个组件在不同浏览器下的显示差异"
通过这些技巧开发者可以充分利用Cursor的AI能力加快前端开发进度提升代码质量和用户体验。
### 5.2 前端项目结构与规范
RuoYi-Vue-Plus (Soybean Admin Pro 版本) 前端项目采用了Vue 3 + TypeScript + Vite的现代化技术栈在二次开发时应遵循以下结构与规范
* **核心目录结构**:
* `src/assets/`: 静态资源文件,如图片、图标和全局样式。
* `src/components/`: 通用组件库包含可复用的UI组件。
* `src/hooks/`: Vue 3 Composition API的自定义hooks封装可复用的逻辑。
* `src/layouts/`: 布局组件,定义整体页面结构。
* `src/router/`: 路由配置,定义应用的导航结构。
* `src/service/api/`: API服务封装与后端的HTTP通信。
* `src/store/`: 使用Pinia的全局状态管理。
* `src/views/`: 页面组件,通常按业务模块组织。
* `src/typings/api/`: TypeScript类型定义特别是API相关的类型。
**使用Cursor进行前端结构分析**
* 分析目录结构:"分析ruoyi-plus-soybean前端项目的目录结构和主要职责"
* 查看组件组织:"ruoyi-plus-soybean中组件是如何组织和复用的"
* 理解技术集成:"项目中Naive UI是如何与Vue 3集成的"
* **前端命名规范**:
* **文件命名**: 组件文件采用PascalCase如`UserForm.vue`),工具/服务类文件采用camelCase如`userService.ts`)。
* **目录命名**: 使用kebab-case如`user-management/`)。
* **组件命名**: 组件名应该始终是多个单词的使用PascalCase以避免与HTML元素冲突如`AppButton`而非`Button`)。
* **Props命名**: 在组件内部props应使用camelCase声明在模板中使用kebab-case传递Vue自动转换
* **事件命名**: 使用kebab-case如`@item-click`),并优先使用原生事件名。
* **代码风格与最佳实践**:
* **Vue 3 Composition API**: 优先使用Composition API + `<script setup>`语法,利用其更好的类型推断和逻辑组织能力。
* **TypeScript类型定义**: 为props、响应式状态、方法参数和返回值提供明确的类型注解避免使用`any`。
* **状态管理**: 局部状态使用`ref`/`reactive`共享状态使用Pinia store。
* **性能优化**: 合理使用`computed`、`v-memo`和组件懒加载,避免不必要的渲染。
* **代码分割**: 路由级别的组件应使用动态导入(`import()`)以实现按需加载。
### 5.3 前端功能模块开发规范
在RuoYi-Vue-Plus前端二次开发中为保持一致性和可维护性应遵循以下规范
* **视图组件设计**:
* **页面结构**: 遵循"搜索区 + 操作区 + 表格/表单区"的经典布局。
* **组件复用**: 将复杂页面拆分为多个小型组件,提高复用性和可维护性。
* **状态提升**: 共享状态应提升到合适的层级避免prop drilling。
* **组件通信**: 使用props和emits进行父子组件通信复杂场景可使用provide/inject或Pinia。
* **API服务与数据处理**:
* **API封装**: 所有后端API调用应在`src/service/api/`下集中管理,按业务模块组织。
* **类型定义**: 请求参数和响应数据都应有明确的TypeScript接口定义位于`src/typings/api/`下。
* **错误处理**: 使用统一的错误处理机制,包括网络错误、业务错误和权限错误。
* **数据转换**: 在API层处理数据转换保证组件接收到的数据格式一致。
* **路由与权限管理**:
* **路由配置**: 新模块路由应定义在`src/router/routes/modules/`下并设置适当的元信息如title、icon、权限等
* **权限控制**: 使用`useAuth`钩子和权限指令控制UI元素的显示/隐藏,路由守卫控制页面访问权限。
* **菜单集成**: 确保路由配置中包含正确的meta信息使其能被自动集成到系统菜单中。
* **样式与主题**:
* **样式隔离**: 组件样式应使用scoped或CSS模块化方案避免全局污染。
* **主题兼容**: 支持深色模式和明亮模式使用CSS变量实现主题切换。
* **响应式设计**: 页面布局应考虑不同设备尺寸,使用响应式设计原则。
* **国际化**:
* **文本外部化**: 所有用户可见文本都应通过`$t`函数从语言包获取,而不是硬编码。
* **语言包组织**: 新模块的翻译应添加到`src/locales/langs/`下对应的语言文件中。
* **状态管理**:
* **Pinia Store**: 模块级共享状态应创建独立的Pinia store位于`src/store/modules/`下。
* **状态持久化**: 需要持久化的状态应使用storage插件保存到localStorage或sessionStorage。
* **异常与加载状态**:
* **加载指示**: 使用骨架屏、加载动画等提供视觉反馈。
* **空状态处理**: 数据为空时显示友好的空状态提示,而非空白界面。
* **错误处理**: 捕获并友好展示操作错误,提供重试机制。
### 5.4 前端组件开发案例
让我们以一个典型的"产品管理"模块为例展示如何使用Cursor辅助开发前端组件
1. **创建API服务**:
* 请求Cursor"基于RuoYi-Vue-Plus规范创建产品管理模块的API服务文件包含列表查询、详情、新增、修改和删除方法"
* 得到productApi.ts文件包含完整的API调用方法和类型定义。
2. **创建列表页组件**:
* 请求Cursor"基于Naive UI和项目规范创建一个产品列表页组件包含搜索条件、操作按钮和数据表格"
* 得到ProductList.vue组件实现了完整的列表功能。
3. **创建表单组件**:
* 请求Cursor"创建一个产品表单组件,用于新增和编辑产品信息,包含必要的字段验证"
* 得到ProductForm.vue组件可在对话框或抽屉中使用。
4. **创建路由配置**:
* 请求Cursor"为产品管理模块创建路由配置,包含列表页和详情页"
* 得到product.ts路由配置文件可整合到系统菜单中。
5. **状态管理**:
* 请求Cursor"创建产品管理的Pinia store管理产品列表和筛选条件状态"
* 得到useProductStore.ts实现产品数据的集中管理。
通过Cursor的辅助可以快速生成符合项目规范的代码大幅提高开发效率。
### 5.5 前后端交互最佳实践
在RuoYi-Vue-Plus项目中前后端交互是开发过程中的重要环节应遵循以下最佳实践
* **统一的API调用封装**:
* 使用项目提供的封装工具axios或alova处理HTTP请求。
* 统一处理请求头、认证令牌、响应状态和错误。
* 按后端API端点组织前端API服务文件。
* **请求与响应类型定义**:
* 为每个API请求定义明确的参数类型和响应类型。
* 与后端BO、VO对象保持一致确保类型安全。
* 在TypeScript接口中添加必要的注释提高代码可读性。
* **请求状态管理**:
* 使用hooks如useRequest管理请求状态包括loading、error和data。
* 实现请求取消、防抖、节流等优化机制。
* 合理处理并发请求和请求依赖关系。
* **数据缓存策略**:
* 适当使用客户端缓存减少重复请求,提高用户体验。
* 实现数据刷新机制,确保数据及时更新。
* 考虑使用service worker或localStorage进行离线数据存储。
* **错误处理与重试**:
* 实现统一的错误处理机制,友好展示错误信息。
* 针对网络错误提供自动或手动重试机制。
* 区分处理业务错误和技术错误。
* **表单提交与验证**:
* 实现前端表单验证,减轻后端验证压力。
* 使用统一的表单提交方法,处理提交状态和响应。
* 适当使用防重复提交机制,避免用户误操作。
* **文件上传与下载**:
* 封装统一的文件上传组件,支持进度展示、大文件分片上传等。
* 实现文件下载功能,支持直接下载和流式下载。
* 处理文件格式验证和大小限制。
通过遵循这些前后端交互最佳实践可以确保前端应用与后端API的无缝集成提高开发效率和用户体验。
## 6. 前后端协作规范
为了确保RuoYi-Vue-Plus二次开发过程中前后端协作的高效与顺畅本章节提供了一套协作规范与最佳实践。
### 6.1 接口设计与文档规范
在RuoYi-Vue-Plus项目中良好的接口设计与规范文档是前后端协作的基础
* **RESTful API设计**:
* 遵循HTTP方法语义GET查询、POST创建、PUT更新、DELETE删除
* 使用资源为中心的URL设计如`/api/system/user/{userId}`。
* 使用HTTP状态码正确表达请求结果200成功、400客户端错误、500服务器错误等
* 保持接口的向后兼容性,避免随意修改已发布接口的参数结构。
* **接口文档规范**:
* 使用Swagger/Knife4j生成规范的API文档包含完整的接口信息、参数说明和响应结构。
* 对每个接口添加详细注释,说明功能、使用场景、参数要求和业务规则。
* 及时更新接口文档,确保与实际代码保持一致。
* 在Swagger注解中明确标识接口的权限要求、是否需要认证等信息。
* **请求与响应格式**:
* 请求参数应符合后端BO对象的结构必填字段明确标识。
* 响应统一使用RuoYi-Vue-Plus的`R<T>`通用返回结构,包含业务状态码、提示信息和数据。
* 分页查询接口应返回标准化的分页信息结构,包含总记录数、当前页数据等。
* 使用恰当的数据类型特别是日期时间ISO 8601格式和数值类型。
### 6.2 开发协作流程
有效的协作流程可以大幅提高前后端团队的开发效率:
* **接口优先原则**:
* 在开发新功能前先由前后端共同确定API接口规范包括URL、参数结构、响应格式等。
* 后端先完成接口的基本骨架实现并提供Mock数据前端可以据此并行开发。
* 使用Swagger/Knife4j作为接口契约前后端基于此进行独立开发。
* **代码评审与联调**:
* 实施定期代码评审,确保代码质量和一致性。
* 安排专门的联调时间,解决前后端交互问题。
* 建立明确的Bug反馈与修复流程快速响应问题。
* **使用Cursor辅助协作**:
* 利用Cursor分析后端实现"分析这个用户管理接口的参数结构和响应格式"
* 让Cursor生成前端调用代码"基于这个Swagger文档生成用户管理模块的API调用代码"
* 使用Cursor同步修改"后端修改了用户查询接口更新对应的前端API调用和类型定义"
### 6.3 数据结构一致性
前后端数据结构的一致性是无缝集成的关键:
* **类型映射**:
* 建立Java与TypeScript类型的明确映射关系如Java的Long/Integer对应TS的numberString对应string等。
* 对于特殊类型(如日期时间、枚举值)定义统一的序列化与反序列化规则。
* 前端TypeScript接口应与后端BO/VO对象结构保持一致字段名称和类型对应。
* **命名一致性**:
* API路径、参数名和响应字段应使用一致的命名风格推荐使用小驼峰式camelCase
* 保持前后端对业务概念的命名一致性,避免同一实体在前后端使用不同名称。
* 在代码注释中使用一致的业务术语表述。
* **数据同步策略**:
* 使用Swagger生成的接口文档导出API定义前端可以基于此自动生成TypeScript类型定义。
* 后端接口变更时,及时通知前端并更新对应的类型定义。
* 考虑使用代码生成工具自动保持前后端数据结构的同步。
### 6.4 异常处理与状态码规范
统一的异常处理机制能够提高系统的可靠性和用户体验:
* **业务状态码**:
* 使用RuoYi-Vue-Plus统一的业务状态码体系不同类型的错误使用不同的状态码范围。
* 确保错误信息清晰明确,便于定位问题。
* 对安全敏感的异常,在生产环境隐藏技术细节,仅展示用户友好的错误信息。
* **前端异常处理**:
* 实现统一的错误拦截器,根据后端返回的状态码进行相应处理。
* 针对不同类型的错误提供不同的用户界面反馈(如表单验证错误、权限错误、系统错误等)。
* 特殊业务错误应有专门的处理逻辑,如令牌过期自动跳转登录页面。
* **后端异常处理**:
* 使用全局异常处理器统一捕获并处理异常,转换为标准的响应格式。
* 区分业务异常和系统异常,业务异常应给出清晰的错误原因。
* 敏感操作应有完整的日志记录,便于问题追踪。
## 7. 代码规范与风格
在RuoYi-Vue-Plus项目的二次开发中遵循统一的代码规范和风格可以提高代码质量、可维护性和团队协作效率。
### 7.1 后端代码规范
* **Java编码风格**:
* 遵循阿里巴巴Java开发手册规范结合RuoYi-Vue-Plus项目的特定约定。
* 使用统一的代码格式化配置如缩进4个空格、行宽最大120字符等。
* 遵循Java类和方法的命名约定类名使用PascalCase方法名和变量名使用camelCase。
* 方法名应反映其功能和意图,如`getUserById`、`updateUserStatus`等。
* **代码注释规范**:
* 类级注释每个类都应有Javadoc注释说明其目的、功能和特殊说明。
* 方法注释公共方法应有完整的Javadoc注释包括功能描述、参数说明、返回值和异常。
* 使用`@author`标注代码的作者,便于追踪责任人。
* 对于复杂的业务逻辑或算法,添加详细的行内注释说明。
* **SQL编写规范**:
* 优先使用MyBatis Plus提供的方法避免手写复杂SQL。
* 必要的自定义SQL应放在XML文件中而非注解中便于维护和阅读。
* SQL关键字使用大写表名和字段名使用小写提高可读性。
* 编写复杂查询时应考虑性能,合理使用索引,避免全表扫描。
* **安全编码规范**:
* 所有用户输入必须进行校验和过滤防止XSS、SQL注入等安全漏洞。
* 敏感数据(如密码)必须加密存储,并在传输和展示时脱敏处理。
* 使用参数化查询避免直接拼接SQL语句。
* 权限检查应在所有敏感操作前执行,确保用户只能访问其有权限的资源。
### 7.2 前端代码规范
* **Vue组件风格**:
* 使用Vue 3的Composition API和`<script setup>`
* 组件文件采用单文件组件SFC格式即`.vue`文件。
* 组件名应使用PascalCase并且至少两个单词如`UserProfile`而非`User`)。
* 组件应遵循单一职责原则,过大的组件应拆分为多个小组件。
* **TypeScript规范**:
* 为所有变量、参数、返回值和属性提供明确的类型注解,避免使用`any`。
* 使用接口Interface定义复杂数据结构并添加必要的注释说明。
* 遵循TypeScript的命名约定接口名使用PascalCase并以`I`开头类型别名使用PascalCase变量和函数使用camelCase。
* 利用TypeScript的类型检查能力在编译时捕获潜在问题。
* **CSS/SCSS规范**:
* 使用scoped属性或CSS Modules隔离组件样式避免全局污染。
* 遵循BEMBlock-Element-Modifier或类似的CSS命名规范。
* 复用项目提供的颜色变量、主题变量和布局工具,保持视觉一致性。
* 编写响应式样式,确保在不同设备上的良好表现。
* **前端代码优化**:
* 组件应实现合理的懒加载,特别是大型页面或组件。
* 避免不必要的计算和渲染,合理使用`computed`、`watch`和`v-memo`。
* 对用户输入添加防抖/节流处理避免频繁触发事件或API调用。
* 使用Vue DevTools进行性能分析优化重渲染和内存使用。
### 7.3 使用Cursor提高代码质量
Cursor作为AI编辑助手能显著提高代码质量和开发效率
* **代码审查辅助**:
* 请求Cursor检查代码质量"分析这个Service类有哪些可以改进的地方"
* 进行安全审查:"检查这段代码是否存在安全漏洞"
* 寻找性能优化点:"分析这个查询方法的性能,提供优化建议"
* **代码规范化**:
* 统一编码风格:"根据阿里巴巴Java规范格式化这段代码"
* 自动添加注释:"为这个复杂方法添加完整的Javadoc注释"
* 类型补全:"为这段TypeScript代码添加完整的类型定义"
* **重构与优化**:
* 提取重复逻辑:"从这几个Controller中提取公共验证逻辑"
* 简化复杂方法:"重构这个超过100行的方法提高可读性"
* 优化算法:"这个列表处理算法有更高效的实现方式吗?"
* **学习与最佳实践**:
* 分析设计模式:"这段代码使用了什么设计模式?如何改进?"
* 学习项目风格:"分析RuoYi-Vue-Plus项目的错误处理机制和最佳实践"
* 理解框架特性:"说明MyBatis Plus的性能优化特性及使用方法"
通过利用Cursor的这些功能开发团队可以统一代码风格提高代码质量减少常见错误同时加快开发速度。
## 8. 新模块添加流程与实践
本章节将完整展示如何在RuoYi-Vue-Plus中添加全新的业务模块涵盖从需求分析到最终部署的全流程。
### 8.1 模块需求分析与设计
在开始开发前,需要对新模块进行全面的需求分析与架构设计:
* **需求收集与分析**:
* 明确新模块的业务目标、功能范围和用户角色。
* 绘制业务流程图,理清核心业务规则和数据流向。
* 设计数据模型,确定实体关系和关键属性。
* 识别与现有模块的交互点和依赖关系。
* **使用Cursor辅助设计**:
* 业务流程分析:"基于这个需求描述,绘制产品管理模块的业务流程图"
* 数据模型设计:"根据产品管理需求,设计数据库表结构和实体关系"
* 接口规划:"为产品管理模块设计RESTful API接口清单"
* 模块架构:"设计产品管理模块的整体架构和组件结构"
### 8.2 数据库设计与实现
良好的数据库设计是模块稳定运行的基础:
* **表结构设计**:
* 遵循RuoYi-Vue-Plus的表命名规范如业务表使用模块前缀如`pms_product`)。
* 包含标准审计字段(如创建时间、创建者、更新时间、更新者)。
* 对于多租户场景添加租户ID字段。
* 设计合理的索引,考虑查询性能和数据完整性。
* **SQL脚本编写**:
* 创建数据表的SQL脚本应放在对应的SQL目录下如`script/sql/mysql/pms_xxx.sql`)。
* 编写初始数据插入脚本,包括必要的字典数据、菜单权限等。
* 为升级场景准备增量更新脚本,放在`update`子目录下。
* 包含必要的注释,说明表的用途和关键字段。
* **使用Cursor生成SQL**:
* 请求:"根据产品管理模块需求生成创建产品表和分类表的MySQL SQL脚本"
* 请求:"为产品管理模块生成菜单和权限的初始化SQL"
* 请求:"根据这个ER图优化产品表索引设计"
### 8.3 后端模块开发流程
后端模块开发应遵循RuoYi-Vue-Plus的分层架构和开发规范
1. **创建Maven模块**:
* 在`ruoyi-modules`下创建新的子模块(如`ruoyi-pms`)。
* 配置pom.xml添加必要的依赖项。
* 创建基础包结构controller, service, mapper, domain等
2. **使用代码生成器**:
* 通过RuoYi-Vue-Plus后台的代码生成功能基于数据表生成基础CRUD代码。
* 根据业务需求调整生成的代码,特别是表单验证规则和业务逻辑。
* 生成对应的前端代码包括API服务、视图组件和路由定义。
3. **业务逻辑实现**:
* 依照分层架构实现业务逻辑,严格遵循前面章节的架构约定。
* 实现个性化业务需求,如特殊查询、统计分析、报表导出等。
* 添加单元测试,验证关键业务逻辑的正确性。
4. **接口测试与文档**:
* 使用Swagger/Knife4j生成API文档完善接口描述和参数说明。
* 通过Postman或Knife4j界面测试接口功能验证各种场景下的行为是否符合预期。
* 编写接口测试脚本,便于后续持续集成和回归测试。
### 8.4 前端模块开发流程
前端模块开发应遵循RuoYi-Vue-Plus (Soybean Admin Pro) 的组件化开发理念:
1. **准备工作**:
* 在前端项目中创建对应的目录结构,如`src/views/your-module/`。
* 定义API服务和类型放在`src/service/api/your-module/`和`src/typings/api/your-module/`。
* 创建路由配置,放在`src/router/routes/modules/your-module.ts`。
2. **组件开发**:
* 基于UI设计和业务需求开发必要的页面组件和功能组件。
* 实现数据加载、表单交互、列表展示等核心功能。
* 确保所有组件支持国际化和主题切换。
3. **业务逻辑与状态管理**:
* 根据需求实现前端业务逻辑,如数据处理、校验逻辑等。
* 针对复杂状态创建Pinia store进行集中管理。
* 抽取通用逻辑到hooks提高代码复用率。
4. **测试与优化**:
* 在各种浏览器环境下测试功能,确保兼容性。
* 进行性能优化,如组件懒加载、虚拟滚动等。
* 使用Chrome DevTools分析渲染性能优化瓶颈。
### 8.5 集成与部署
最后,将新模块集成到系统并准备部署:
1. **前后端集成**:
* 进行前后端联调,解决接口对接问题。
* 验证数据流转和业务流程的完整性。
* 修复发现的Bug并优化用户体验。
2. **系统集成**:
* 在系统菜单中添加新模块入口。
* 配置模块权限,确保只有有权限的用户能访问。
* 更新全局类型、工具类等共享资源。
3. **部署准备**:
* 更新项目文档包括README、用户手册等。
* 准备部署脚本和配置文件。
* 执行最终测试,确认所有功能正常运行。
4. **持续集成与部署**:
* 将代码提交到版本控制系统并触发CI/CD流程。
* 执行自动化测试,验证新模块不会破坏现有功能。
* 部署到测试环境或生产环境,并进行监控。
通过遵循这一完整的开发流程并充分利用Cursor提供的AI辅助功能可以高效地将新业务模块集成到RuoYi-Vue-Plus系统中确保代码质量和功能稳定性。
## 9. 总结与最佳实践要点
作为总结本文档提供了RuoYi-Vue-Plus项目二次开发的全面指南特别强调了借助Cursor这一现代AI开发工具提升开发效率的方法。以下是关键最佳实践要点
1. **架构理解与遵循**:
* 深入理解RuoYi-Vue-Plus的分层架构和模块设计。
* 严格遵循既定的架构约定,保持代码结构的一致性。
* 使用Cursor快速分析和理解现有代码结构加速学习曲线。
2. **代码规范与质量**:
* 遵循统一的编码规范和风格指南。
* 利用Cursor进行代码审查和质量提升。
* 注重注释和文档,确保代码可维护性。
3. **模块设计与实现**:
* 新模块应保持职责单一、边界清晰。
* 充分利用代码生成器提高开发效率。
* 确保前后端接口设计的一致性和规范性。
4. **前后端协作**:
* 建立明确的接口契约和开发流程。
* 保持数据结构和命名约定的一致性。
* 利用Cursor辅助API调用和类型生成减少协作成本。
5. **性能与安全考量**:
* 在设计阶段考虑性能和安全因素。
* 实施必要的安全措施,如输入验证、权限控制和数据脱敏。
* 使用Cursor分析性能瓶颈并提供优化建议。
6. **测试与部署**:
* 为核心业务逻辑编写单元测试。
* 实施自动化测试和持续集成。
* 建立规范的部署流程和回滚机制。
通过遵循这些最佳实践结合Cursor的AI辅助能力开发团队可以显著提高在RuoYi-Vue-Plus平台上的二次开发效率和代码质量打造出高性能、高可靠性的企业级应用。

View File

@ -0,0 +1,840 @@
# RuoYi-Vue-Plus项目分析报告
## 1. 项目概述
RuoYi-Vue-Plus是在RuoYi-Vue基础上进行重构升级的开源项目它专为分布式集群与多租户场景设计采用插件化和扩展包的结构形式极大提高了系统的解耦程度和扩展性。项目使用Spring Boot 3.4、JDK 17/21基于Vue3+TS+ElementPlus重写前端并使用Undertow替代Tomcat作为Web容器。
### 1.1 项目特点
- 采用插件化+扩展包形式,结构解耦,易于扩展
- 严格遵守Alibaba规范代码格式统一
- 支持多种数据库MySQL、Oracle、PostgreSQL、SQLServer等
- 支持多租户架构
- 前端使用Vue3+TS+ElementPlus
- 底层基于Redisson的分布式能力分布式锁、限流等
- 使用Sa-Token+JWT实现权限认证
- 使用Mybatis-Plus作为ORM框架
- 内置丰富的功能模块和实用工具
## 2. 项目架构
### 2.1 整体架构
RuoYi-Vue-Plus采用前后端分离的架构模式
- 后端Spring Boot 3.4 + MyBatis-Plus + Sa-Token
- 前端Vue3 + TypeScript + ElementPlus
项目部署架构图:
![Plus部署架构图](https://foruda.gitee.com/images/1678981882624240692/ae2a3f3e_1766278.png)
### 2.2 目录结构
项目主要分为以下几个部分:
#### 2.2.1 后端结构
```
ruoyi-vue-plus
├── ruoyi-admin -- 启动模块,项目入口
├── ruoyi-common -- 通用模块,各种功能组件
│ ├── ruoyi-common-bom -- 依赖版本管理
│ ├── ruoyi-common-core -- 核心功能
│ ├── ruoyi-common-doc -- 接口文档
│ ├── ruoyi-common-encrypt -- 数据加解密
│ ├── ruoyi-common-excel -- Excel处理
│ ├── ruoyi-common-idempotent -- 幂等处理
│ ├── ruoyi-common-job -- 定时任务
│ ├── ruoyi-common-json -- JSON处理
│ ├── ruoyi-common-log -- 日志处理
│ ├── ruoyi-common-mail -- 邮件处理
│ ├── ruoyi-common-mybatis -- ORM配置
│ ├── ruoyi-common-oss -- 对象存储
│ ├── ruoyi-common-ratelimiter -- 限流处理
│ ├── ruoyi-common-redis -- Redis配置
│ ├── ruoyi-common-satoken -- 认证鉴权
│ ├── ruoyi-common-security -- 安全配置
│ ├── ruoyi-common-sensitive -- 数据脱敏
│ ├── ruoyi-common-sms -- 短信服务
│ ├── ruoyi-common-social -- 社交登录
│ ├── ruoyi-common-sse -- SSE推送
│ ├── ruoyi-common-tenant -- 多租户
│ ├── ruoyi-common-translation -- 数据翻译
│ ├── ruoyi-common-web -- Web功能
│ └── ruoyi-common-websocket -- WebSocket
├── ruoyi-extend -- 扩展模块
│ ├── ruoyi-monitor-admin -- 监控管理
│ └── ruoyi-snailjob-server -- 任务调度
├── ruoyi-modules -- 业务模块
│ ├── ruoyi-demo -- 示例模块
│ ├── ruoyi-generator -- 代码生成
│ ├── ruoyi-job -- 定时任务
│ ├── ruoyi-system -- 系统管理
│ └── ruoyi-workflow -- 工作流
└── ruoyi-plus-soybean -- 前端项目
```
#### 2.2.2 前端结构
```
ruoyi-plus-soybean
├── docs -- 文档
├── packages -- 功能包
├── public -- 静态资源
├── src -- 源代码目录
│ ├── assets -- 静态资源
│ ├── components -- 组件
│ ├── constants -- 常量
│ ├── enum -- 枚举
│ ├── hooks -- 钩子函数
│ ├── layouts -- 布局
│ ├── locales -- 国际化
│ ├── plugins -- 插件
│ ├── router -- 路由
│ ├── service -- 服务调用
│ ├── store -- 状态管理
│ ├── styles -- 样式
│ ├── theme -- 主题
│ ├── typings -- 类型定义
│ ├── utils -- 工具类
│ ├── views -- 视图
│ ├── App.vue -- 主组件
│ └── main.ts -- 入口文件
```
## 3. 功能模块
### 3.1 核心功能
#### 基础功能
- **多租户管理**:支持租户套餐、过期时间、用户数量等管理
- **用户权限**:用户、角色、部门、菜单权限管理
- **系统监控**:在线用户、操作日志、登录日志
- **系统管理**:参数设置、字典管理、附件管理
- **系统工具**:代码生成、表单设计、接口文档
#### 扩展功能
- **工作流**:支持复杂审批流程
- **在线构建器**:拖拽式表单生成
- **定时任务**:任务调度管理
- **系统接口**API文档自动生成
- **服务监控**:监控系统资源和性能
- **缓存监控**Redis监控
### 3.2 技术特性
- **数据权限**基于Mybatis-Plus插件的无感式数据权限过滤
- **数据脱敏**:支持注解+Jackson方式的数据脱敏
- **数据加解密**:支持数据库字段级加解密
- **接口加密**动态AES+RSA加密请求体
- **数据翻译**:注解+序列化期间自动翻译
- **多数据源**:支持动态数据源配置和切换
- **分布式锁**基于Redisson的分布式锁
- **分布式任务调度**基于SnailJob的分布式任务调度
- **文件存储**支持MinIO和S3协议的对象存储
## 4. 代码规范
RuoYi-Vue-Plus项目严格遵循以下规范
### 4.1 项目规范
- 严格遵守Alibaba编码规范
- 使用统一的代码格式化配置
- 采用插件化结构,功能模块独立封装
- 按职责分离不同类型的代码(控制器、服务、实体等)
### 4.2 编码规范
#### 后端规范
1. **命名规范**
- 类名大驼峰命名UserController
- 方法名/变量名小驼峰命名getUserInfo
- 常量全大写下划线分隔MAX_COUNT
2. **包结构**
- controller控制器
- service服务层
- mapper数据访问层
- domain实体类entity、vo、bo、dto等
- util工具类
3. **注释要求**
- 类注释:说明类的用途
- 方法注释:说明方法功能、参数和返回值
- 关键代码注释:解释复杂逻辑
#### 前端规范
1. **命名规范**
- 组件名大驼峰命名UserForm
- 文件名kebab-caseuser-form.vue
- 变量/方法小驼峰命名getUserInfo
2. **目录结构**
- components组件
- views页面
- api接口定义
- utils工具函数
- store状态管理
3. **编码风格**
- 使用TypeScript进行类型检查
- 组件使用组合式API (Composition API)
- 样式使用SCSS并遵循BEM规范
### 4.3 常见代码示例
#### 4.3.1 控制器规范示例
```java
/**
* 用户信息控制器
*
* @author ruoyi-vue-plus
*/
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController {
@Autowired
private ISysUserService userService;
/**
* 获取用户列表
*
* @param user 查询参数
* @return 用户列表
*/
@SaCheckPermission("system:user:list")
@GetMapping("/list")
public TableDataInfo<SysUserVo> list(SysUserBo user) {
startPage();
List<SysUserVo> list = userService.selectUserList(user);
return getDataTable(list);
}
/**
* 新增用户
*
* @param user 用户信息
* @return 结果
*/
@SaCheckPermission("system:user:add")
@Log(title = "用户管理", businessType = BusinessType.INSERT)
@PostMapping
public R<Void> add(@Validated @RequestBody SysUserBo user) {
return toAjax(userService.insertUser(user));
}
}
```
#### 4.3.2 服务接口和实现规范示例
```java
/**
* 用户服务接口
*
* @author ruoyi-vue-plus
*/
public interface ISysUserService {
/**
* 查询用户列表
*
* @param user 查询参数
* @return 用户列表
*/
List<SysUserVo> selectUserList(SysUserBo user);
/**
* 新增用户
*
* @param user 用户信息
* @return 结果
*/
int insertUser(SysUserBo user);
}
/**
* 用户服务接口实现
*
* @author ruoyi-vue-plus
*/
@Service
public class SysUserServiceImpl implements ISysUserService {
@Autowired
private SysUserMapper userMapper;
/**
* 查询用户列表
*
* @param user 查询参数
* @return 用户列表
*/
@Override
public List<SysUserVo> selectUserList(SysUserBo user) {
return userMapper.selectUserList(user);
}
/**
* 新增用户
*
* @param user 用户信息
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int insertUser(SysUserBo user) {
// 业务逻辑实现
return userMapper.insert(user.toEntity());
}
}
```
#### 4.3.3 实体类规范示例
```java
/**
* 用户实体类
*
* @author ruoyi-vue-plus
*/
@Data
@TableName("sys_user")
@KeySequence("sys_user_seq")
public class SysUser extends TenantEntity {
/**
* 用户ID
*/
@TableId(value = "user_id", type = IdType.ASSIGN_ID)
private Long userId;
/**
* 用户账号
*/
private String userName;
/**
* 用户昵称
*/
private String nickName;
/**
* 用户类型sys_user系统用户
*/
private String userType;
/**
* 手机号码
*/
@Sensitive(strategy = SensitiveStrategy.PHONE)
private String phonenumber;
/**
* 帐号状态0正常 1停用
*/
private String status;
}
```
#### 4.3.4 前端页面示例 (Vue3 + TS)
```typescript
<template>
<div class="app-container">
<!-- 搜索表单 -->
<el-form :model="queryParams" ref="queryFormRef" :inline="true">
<el-form-item label="用户名称" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮区域 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
v-hasPermi="['system:user:add']"
@click="handleAdd"
>新增</el-button>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="userList">
<el-table-column label="用户编号" prop="userId" />
<el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
<el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" prop="phonenumber" :show-overflow-tooltip="true" />
<el-table-column label="状态" prop="status">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
type="text"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:user:edit']"
>修改</el-button>
<el-button
type="text"
@click="handleDelete(scope.row)"
v-hasPermi="['system:user:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { listUser, delUser } from '@/api/system/user';
// 定义数据
const loading = ref(false);
const total = ref(0);
const userList = ref([]);
const queryFormRef = ref<any>(null);
// 查询参数
const queryParams = ref({
pageNum: 1,
pageSize: 10,
userName: undefined,
phonenumber: undefined,
});
/** 查询用户列表 */
function getList() {
loading.value = true;
listUser(queryParams.value).then(response => {
userList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
queryFormRef.value.resetFields();
handleQuery();
}
/** 新增按钮操作 */
function handleAdd() {
// 实现新增逻辑
}
/** 修改按钮操作 */
function handleUpdate(row) {
// 实现修改逻辑
}
/** 删除按钮操作 */
function handleDelete(row) {
// 实现删除逻辑
}
onMounted(() => {
getList();
});
</script>
```
## 5. 二次开发指南
### 5.1 开发环境准备
1. **基础环境**
- JDK 17/21
- Maven 3.8+
- Node.js 16+
- MySQL 8.0+/Oracle/PostgreSQL/SQLServer
- Redis 5.0+
2. **IDE推荐**
- IntelliJ IDEA后端
- VSCode前端
### 5.2 开发规范与原则
1. **遵循现有架构**
- 保持与现有代码风格一致
- 保持模块的独立性和可插拔性
- 不随意修改核心模块代码
2. **扩展而非修改**
- 通过扩展现有组件实现功能
- 避免直接修改框架核心代码
3. **注重代码质量**
- 编写单元测试
- 遵循代码规范
- 注释完善
### 5.3 增加PMS模块实施步骤
以下是添加一个产品管理系统(PMS)模块的具体步骤:
#### 5.3.1 后端开发
1. **创建模块结构**
```
ruoyi-modules
└── ruoyi-pms -- PMS模块
├── src/main/java/org/dromara/pms
│ ├── controller -- 控制器
│ ├── domain -- 实体类
│ │ ├── bo -- 业务对象
│ │ ├── entity -- 数据库实体
│ │ └── vo -- 视图对象
│ ├── mapper -- MyBatis接口
│ └── service -- 服务实现
└── src/main/resources
├── mapper -- MyBatis XML
└── i18n -- 国际化资源
```
2. **配置模块POM**
- 创建pom.xml添加必要依赖
- 在父模块中添加新模块引用
3. **创建数据库表**
- 设计表结构
- 编写SQL脚本
4. **开发核心功能**
- 使用代码生成器生成基础CRUD代码
- 扩展实现具体业务逻辑
- 添加权限控制
- 实现多租户和数据权限
- 添加接口文档注释
#### 5.3.2 前端开发
1. **创建模块目录**
```
ruoyi-plus-soybean/src/views/pms
├── product -- 产品管理
├── category -- 分类管理
└── inventory -- 库存管理
```
2. **API接口定义**
```
ruoyi-plus-soybean/src/service/api/pms
├── product.ts -- 产品API
├── category.ts -- 分类API
└── inventory.ts -- 库存API
```
3. **配置路由**
- 在router/routes目录下创建pms.ts
- 在router/index.ts中导入并注册路由
4. **开发页面组件**
- 列表页面
- 表单页面
- 详情页面
5. **添加权限控制**
- 配置菜单与按钮权限
- 实现页面级与按钮级权限控制
#### 5.3.3 集成与测试
1. **数据库脚本集成**
- 将建表SQL添加到初始化脚本中
2. **菜单配置**
- 通过系统管理-菜单管理添加PMS模块菜单
- 配置菜单权限
3. **角色授权**
- 为相关角色分配PMS模块权限
4. **单元测试**
- 编写API测试
- 编写服务层测试
5. **集成测试**
- 测试完整业务流程
- 测试与其他模块的交互
### 5.4 使用框架核心功能的最佳实践
#### 5.4.1 使用多租户
1. **实体类继承TenantEntity**
```java
@Data
@TableName("pms_product")
public class PmsProduct extends TenantEntity {
// 实体字段定义
}
```
2. **多租户过滤配置**
```java
@Configuration
public class TenantConfig {
/**
* 配置需要进行多租户过滤的表
*/
@Bean
public TenantLineHandler tenantLineHandler() {
return new PlusTenantLineHandler() {
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
// 配置不需要过滤的表
return ArrayUtil.contains(IGNORE_TENANT_TABLES, tableName);
}
};
}
}
```
#### 5.4.2 使用数据权限
1. **在Mapper接口上使用注解**
```java
@DataPermission({
@DataColumn(key = "deptName", value = "d.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
public interface SysUserMapper extends BaseMapperPlus<SysUserMapper, SysUser, SysUserVo> {
// 方法定义
}
```
2. **在查询方法上使用注解**
```java
@Override
@DataScope(userAlias = "u", deptAlias = "d")
public List<SysUserVo> selectUserList(SysUserBo user) {
return baseMapper.selectUserList(user);
}
```
#### 5.4.3 使用数据脱敏
```java
public class UserVo {
// 手机号码脱敏
@Sensitive(strategy = SensitiveStrategy.PHONE)
private String phonenumber;
// 邮箱脱敏
@Sensitive(strategy = SensitiveStrategy.EMAIL)
private String email;
// 身份证脱敏
@Sensitive(strategy = SensitiveStrategy.ID_CARD)
private String idCard;
}
```
#### 5.4.4 使用Excel导入导出
```java
@Data
@ExcelIgnoreUnannotated
public class UserImportVo {
@ExcelProperty(value = "用户编号")
private Long userId;
@ExcelProperty(value = "用户名称")
@ExcelRequired
private String userName;
@ExcelProperty(value = "用户昵称")
@ExcelRequired
private String nickName;
@ExcelProperty(value = "手机号码")
@ExcelRequired
private String phonenumber;
@ExcelProperty(value = "邮箱")
private String email;
}
@RestController
public class UserController {
/**
* 导出用户
*/
@SaCheckPermission("system:user:export")
@Log(title = "用户管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, SysUserBo user) {
List<SysUserVo> list = userService.selectUserList(user);
ExcelUtil.exportExcel(list, "用户数据", SysUserVo.class, response);
}
/**
* 导入用户
*/
@SaCheckPermission("system:user:import")
@Log(title = "用户管理", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public R<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport) throws Exception {
ExcelResult<UserImportVo> result = ExcelUtil.importExcel(file, UserImportVo.class);
userService.importUser(result.getList(), updateSupport);
return R.ok();
}
}
```
#### 5.4.5 使用接口幂等性控制
```java
@RestController
@RequestMapping("/system/user")
public class SysUserController {
/**
* 新增用户
*/
@SaCheckPermission("system:user:add")
@Log(title = "用户管理", businessType = BusinessType.INSERT)
@RepeatSubmit(interval = 5000) // 5秒内不允许重复提交
@PostMapping
public R<Void> add(@Validated @RequestBody SysUserBo user) {
return toAjax(userService.insertUser(user));
}
}
```
#### 5.4.6 使用Redis缓存
```java
@Service
public class SysConfigServiceImpl implements ISysConfigService {
/**
* 查询参数配置信息
*
* @param configId 参数配置ID
* @return 参数配置信息
*/
@Override
@Cacheable(cacheNames = CacheNames.SYS_CONFIG, key = "#configId")
public SysConfig selectConfigById(Long configId) {
return baseMapper.selectById(configId);
}
/**
* 新增参数配置
*
* @param config 参数配置信息
* @return 结果
*/
@Override
@CachePut(cacheNames = CacheNames.SYS_CONFIG, key = "#config.configId")
public int insertConfig(SysConfig config) {
return baseMapper.insert(config);
}
/**
* 删除参数配置
*
* @param configId 参数ID
* @return 结果
*/
@Override
@CacheEvict(cacheNames = CacheNames.SYS_CONFIG, key = "#configId")
public int deleteConfigById(Long configId) {
return baseMapper.deleteById(configId);
}
}
```
## 6. 总结
RuoYi-Vue-Plus是一个功能完善、架构清晰的企业级应用开发框架其插件化设计和模块化结构使其非常适合二次开发。在进行二次开发时应当遵循项目的架构设计和编码规范通过扩展而非修改的方式实现业务需求确保系统的可维护性和可扩展性。
通过合理利用项目提供的代码生成、多租户、权限控制等特性,可以大幅提高开发效率,专注于业务逻辑的实现,而非底层架构的搭建。
## 7. 常见问题与解决方案
### 7.1 多租户问题
**问题**:如何对特定表或操作排除多租户过滤?
**解决方案**
- 全局排除在TenantConfig中的ignoreTable方法中添加表名
- 局部排除:使用@TenantIgnore注解标注在方法或类上
```java
@TenantIgnore
public List<SysTenant> selectTenantList(SysTenant tenant) {
return baseMapper.selectList(buildQueryWrapper(tenant));
}
```
### 7.2 权限问题
**问题**:如何实现细粒度的数据权限控制?
**解决方案**
- 使用@DataScope注解并配置用户、部门别名
- 在Mapper层使用@DataPermission定义权限字段
- 在XML中引用权限过滤片段
### 7.3 分布式事务问题
**问题**:如何处理跨服务的分布式事务?
**解决方案**
- 对于强一致性需求使用Seata进行分布式事务管理
- 对于最终一致性需求,考虑使用本地消息表+定时任务或消息队列实现
- 避免长事务,将业务拆分成多个小事务
### 7.4 性能优化问题
**问题**:系统运行缓慢,如何优化?
**解决方案**
- 使用Redis缓存热点数据减少数据库访问
- 优化SQL查询避免全表扫描
- 使用分页查询代替全量查询
- 合理使用索引
- 利用多级缓存(本地缓存+Redis缓存
- 考虑使用读写分离或分库分表

107
docs/GIT_MANUAL.md Normal file
View File

@ -0,0 +1,107 @@
# RuoYi-Vue-Plus 多仓库 Git 管理手册
## 目录
1. [项目架构说明](#项目架构说明)
2. [初始化设置](#初始化设置)
3. [日常操作流程](#日常操作流程)
4. [冲突处理](#冲突处理)
5. [命令速查表](#命令速查表)
6. [注意事项](#注意事项)
## 项目架构说明
- **主仓库**`ruoyi-vue-plus`
- 同步源:`https://github.com/dromara/RuoYi-Vue-Plus`
- 推送目标:`https://github.com/figo990/RuoYi-Vue-Plus`
- **子模块**`ruoyi-plus-soybean`
- 同步源:`https://gitee.com/xlsea/ruoyi-plus-soybean`
- 与主仓库一起推送到 `figo990/RuoYi-Vue-Plus`
## 初始化设置
```bash
# 1. 克隆主仓库
git clone https://github.com/figo990/RuoYi-Vue-Plus.git ruoyi-vue-plus
cd ruoyi-vue-plus
# 2. 添加官方仓库为上游源
git remote add upstream https://github.com/dromara/RuoYi-Vue-Plus.git
# 3. 初始化子模块
git submodule add https://gitee.com/xlsea/ruoyi-plus-soybean.git ruoyi-plus-soybean
```
## 日常操作流程
### 同步主仓库更新
```bash
git fetch upstream
git merge upstream/main
git push origin main
```
### 同步子模块更新
```bash
cd ruoyi-plus-soybean
git pull origin main
cd ..
git add ruoyi-plus-soybean
git commit -m "Update submodule"
git push origin main
```
### 提交本地修改
```bash
# 修改主仓库
git add .
git commit -m "修改说明"
git push origin main
# 修改子模块
cd ruoyi-plus-soybean
git add .
git commit -m "子模块修改说明"
git push origin main
cd ..
git add ruoyi-plus-soybean
git commit -m "更新子模块引用"
git push origin main
```
## 冲突处理
### 主仓库冲突
```bash
git fetch upstream
git merge upstream/main
# 手动解决冲突后
git add .
git commit -m "解决冲突"
git push origin main
```
### 子模块冲突
```bash
cd ruoyi-plus-soybean
git pull origin main
# 手动解决冲突后
git add .
git commit -m "解决子模块冲突"
git push origin main
cd ..
git add ruoyi-plus-soybean
git commit -m "更新子模块引用"
git push origin main
```
## 命令速查表
| 操作 | 命令 |
| -------------- | ----------------------------------------------------------------------------------- |
| 拉取官方更新 | `git fetch upstream && git merge upstream/main` |
| 推送主仓库修改 | `git add . && git commit -m "msg" && git push origin main` |
| 更新子模块 | `cd ruoyi-plus-soybean && git pull origin main` |
| 提交子模块修改 | `cd ruoyi-plus-soybean && git add . && git commit -m "msg" && git push origin main` |
## 注意事项
1. 首次克隆后需初始化子模块:
```bash
git submodule update --init --recursive
```
2. 子模块修改需单独提交到其仓库
3. 保持主仓库和子模块分支一致(推荐使用`main`分支)

View File

@ -0,0 +1,754 @@
## 云宿居 PMS 系统 - PMS核心数据模型 (最终版 v_ry_final_4.0)
**快速导航:**
- [1. 引言](#1-引言)
- [2. PMS 核心E-R图 (最终版 - 简化)](#2-pms-核心er图-最终版---简化)
- [3. 表结构定义](#3-表结构定义)
- [3.1 SaaS平台与系统基础表 (源自若依)](#31-saas平台与系统基础表-源自若依)
- [3.2 PMS核心业务表](#32-pms核心业务表)
- [3.3 公共数据模型 (PMS相关)](#33-公共数据模型-pms相关)
- [4. 主要业务实体状态流转](#4-主要业务实体状态流转)
- [5. 核心业务枚举值定义 (PMS主要部分)](#5-核心业务枚举值定义-pms主要部分)
- [6. 索引命名与规范建议](#6-索引命名与规范建议)
## 1. 引言
### 1.1 文档目的与范围
本文档详细定义了"云宿居"民宿 Property Management System (PMS) 的数据库逻辑模型,并与其依赖的若依框架的基础表(如租户、用户、部门等)进行了整合。本文档旨在为数据库的物理实现和后端开发提供具体指导。
**核心原则:**
1. 若依框架的基础实体(用户、租户、角色、部门等)及其字段规范固定不变。
2. PMS新增的核心业务表主键统一采用 `BIGINT` 自增ID。
3. **PMS业务表只关联部门(`dept_id`),不直接关联租户(`tenant_id`)。部门代表实际的门店/分店,通过部门可以查询到其所属租户信息。** 这使得"一个租户下有多个门店"的业务模型更加清晰,同时减少数据冗余。
4. **特例:`pms_tenant_settings`和`pms_mp_settings`表保留tenant_id字段通过dept_id为NULL来表示租户全局设置通过具体的dept_id值表示部门/门店特定设置。**
5. PMS业务表中的枚举字段统一使用 `VARCHAR(50)` 存储描述性字符串以增强可读性和扩展性。这与若依常用的 `CHAR(1)` 枚举在交互时需注意API层面的适配。
### 1.2 命名与设计约定 (PMS新增表部分)
* **表命名:** `pms_` (PMS核心业务表)`cmn_` (公共模块)。若依表使用其原生 `sys_` 前缀。
* **字段命名:** `snake_case` (小写下划线)。
* **主键 (PK):**
* PMS核心业务实体: **`BIGINT` AUTO_INCREMENT**。
* 日志类高频写入表: `BIGINT` AUTO_INCREMENT。
* **部门ID `dept_id`:** `bigint(20)` (同 `sys_dept.dept_id`)在PMS业务表中非空用于标识数据归属于哪个门店。
* **外键 (FK):** 类型与关联表主键一致。`COMMENT '关联 table_name.column_name'`。
* **通用审计字段 (对齐若依):**
* `create_by` (BIGINT(20) NULLABLE, COMMENT '创建者,关联 sys_user.user_id')
* `create_time` (DATETIME NULLABLE, COMMENT '创建时间')
* `update_by` (BIGINT(20) NULLABLE, COMMENT '更新者,关联 sys_user.user_id')
* `update_time` (DATETIME NULLABLE, COMMENT '更新时间')
* `create_dept_id` (BIGINT(20) NULLABLE, COMMENT '创建记录的操作员所属部门,关联 sys_dept.dept_id')
* **软删除策略 (对齐若依):**
* `del_flag` (CHAR(1) DEFAULT '0' NOT NULL, COMMENT '删除标志0代表存在 1代表删除')。
* **状态与枚举字段 (PMS优化):**
* PMS业务表中的枚举字段统一使用 `VARCHAR(50)` 存储描述性枚举字符串。
## 2. PMS 核心E-R图 (最终版 - 简化)
```mermaid
erDiagram
%% --- 若依基础表 (关键部分) ---
sys_tenant {
varchar(20) tenant_id PK "租户编号 (业务主键)"
bigint id "物理主键, 若依内部使用"
varchar(255) company_name "企业/民宿名称"
}
sys_dept {
bigint dept_id PK "部门ID"
varchar(20) tenant_id FK "关联 sys_tenant.tenant_id"
varchar(30) dept_name "部门名称"
}
sys_user {
bigint user_id PK "用户ID (PMS员工)"
varchar(20) tenant_id FK "用户初始归属租户"
bigint dept_id FK "用户所属部门"
varchar(30) user_name "用户名, 全局唯一"
}
sys_role {
bigint role_id PK "角色ID"
varchar(20) tenant_id FK "角色定义所属租户 (或平台)"
varchar(30) role_name "角色名称"
}
%% --- Common Contact Table ---
cmn_contacts {
BIGINT contact_id PK "联系人ID (自增)"
BIGINT dept_id FK "所属部门ID"
VARCHAR(255) full_name "姓名"
}
%% --- PMS Core Business Tables (主键改为BIGINT) ---
pms_room_types {
BIGINT room_type_id PK "房型ID (自增)"
BIGINT dept_id FK "部门ID"
VARCHAR(255) name "房型名称"
}
pms_room_rooms {
BIGINT room_id PK "房间ID (自增)"
BIGINT dept_id FK "部门ID"
BIGINT room_type_id FK "房型ID"
VARCHAR(255) room_number "房间号"
VARCHAR(50) room_status "物理状态"
VARCHAR(50) cleaning_status "清洁状态"
}
pms_room_locks {
BIGINT lock_id PK "锁定ID (自增)"
BIGINT dept_id FK "部门ID"
BIGINT room_id FK "房间ID"
}
pms_core_orders {
BIGINT order_id PK "订单ID (自增)"
BIGINT dept_id FK "部门ID"
BIGINT contact_id FK "联系人ID"
BIGINT room_type_id FK "房型ID"
BIGINT pms_room_id FK "房间ID(可空)"
VARCHAR(50) order_status "订单状态"
VARCHAR(50) order_source "订单来源"
}
pms_core_order_items {
BIGINT order_item_id PK "订单项ID (自增)"
BIGINT order_id FK "订单ID"
BIGINT dept_id FK "部门ID"
VARCHAR(50) product_type "产品类型"
}
pms_core_channels {
BIGINT channel_id PK "渠道ID (自增)"
BIGINT dept_id FK "部门ID(可空)"
VARCHAR(50) channel_type "渠道类型"
}
pms_finance_folios {
BIGINT folio_id PK "账单ID (自增)"
BIGINT dept_id FK "部门ID"
BIGINT order_id FK "订单ID(UNIQUE)"
VARCHAR(50) folio_status "账单状态"
}
pms_finance_transactions {
BIGINT transaction_id PK "交易ID (自增)"
BIGINT folio_id FK "账单ID"
BIGINT dept_id FK "部门ID"
VARCHAR(50) transaction_type "交易类型"
}
pms_finance_payment_methods {
BIGINT payment_method_id PK "支付方式ID (自增)"
BIGINT dept_id FK "部门ID"
VARCHAR(50) method_type "支付方式类型"
}
pms_finance_extra_charge_items {
BIGINT item_id PK "附加费用项ID (自增)"
BIGINT dept_id FK "部门ID"
}
pms_room_pricing_rules {
BIGINT rule_id PK "价格规则ID (自增)"
BIGINT dept_id FK "部门ID"
BIGINT room_type_id FK "房型ID(可空)"
}
pms_tenant_settings {
BIGINT setting_id PK "设置ID (自增)"
BIGINT dept_id FK "部门ID"
VARCHAR(100) setting_group "设置分组"
VARCHAR(100) setting_key "设置键名"
}
pms_mp_settings {
BIGINT setting_id PK "小程序设置ID (自增)"
BIGINT dept_id FK "部门ID"
VARCHAR(100) setting_key "设置键名"
}
pms_tenant_user_devices {
BIGINT id PK "设备记录ID (自增)"
BIGINT user_id FK "用户ID"
}
cmn_contact_tags {
BIGINT tag_id PK "标签ID (自增)"
BIGINT dept_id FK "部门ID"
}
cmn_contact_tag_relations {
BIGINT relation_id PK "关联ID (自增)"
BIGINT contact_id FK "联系人ID"
BIGINT tag_id FK "标签ID"
}
%% --- 若依基础表关联 ---
sys_tenant ||--o{ sys_dept : "包含门店/部门"
sys_tenant ||--o{ sys_user : "的主要租户"
sys_tenant ||--o{ sys_role : "定义角色归属"
sys_dept ||--o{ sys_user : "员工属于门店"
%% --- PMS 表关联到若依基础表 ---
sys_dept ||--o{ pms_room_types : "定义房型"
sys_dept ||--o{ pms_room_rooms : "拥有房间"
sys_dept ||--o{ pms_core_orders : "拥有订单"
sys_dept ||--o{ cmn_contacts : "拥有联系人"
sys_dept ||--o{ pms_tenant_settings : "拥有PMS配置"
sys_dept ||--o{ pms_mp_settings : "拥有小程序配置"
sys_dept ||--o{ cmn_contact_tags : "定义联系人标签"
sys_user ||--o{ pms_tenant_user_devices : "注册设备"
%% --- PMS 内部表关联 ---
pms_room_types ||--o{ pms_room_rooms : "有具体房间"
pms_room_types ||--o{ pms_core_orders : "被预订类型"
pms_room_types o|--o{ pms_room_pricing_rules : "规则适用房型"
pms_room_rooms ||--o{ pms_room_locks : "可被锁定"
pms_room_rooms o|--o{ pms_core_orders : "被分配房间"
pms_room_rooms o|--o{ pms_core_order_items : "关联房晚项目"
pms_core_orders ||--o{ pms_core_order_items : "包含项目"
pms_core_orders ||--|| pms_finance_folios : "生成账单"
cmn_contacts o|--o{ pms_core_orders : "是主要联系人"
pms_core_channels o|--o{ pms_core_orders : "是来源渠道"
pms_finance_folios ||--o{ pms_finance_transactions : "记录交易"
pms_finance_payment_methods o|--o{ pms_finance_transactions : "使用支付方式"
pms_finance_extra_charge_items o|--o{ pms_core_order_items : "可以是附加项"
cmn_contacts ||--o{ cmn_contact_tag_relations : "关联标签"
cmn_contact_tags ||--o{ cmn_contact_tag_relations : "被应用于"
```
*注E-R图中部分表的字段为简化表示详细字段请参照下文。*
## 3. 表结构定义
### 3.1 SaaS平台与系统基础表 (源自若依)
这些表结构直接采用或参考附件 `ry_vue_5.X - 副本.txt` 中的定义,关键表包括:
* `sys_tenant`: 租户表 (业务主键 `tenant_id varchar(20)`)
* `sys_dept`: 部门表 (主键 `dept_id bigint(20)`),在本系统中将作为门店/分店使用
* `sys_user`: 用户信息表 (主键 `user_id bigint(20)`)
* `sys_role`: 角色信息表 (主键 `role_id bigint(20)`)
* `sys_user_role`: 用户角色关联表
* `sys_config`: 参数配置表
* `sys_logininfor`: 系统访问记录(登录日志)
* `sys_dict_type`, `sys_dict_data`: 字典表 (PMS枚举值优先在PMS表中使用 `VARCHAR(50)`,但若依系统本身的字典可按其规范使用)
**PMS系统将直接使用或关联这些若依表中的记录例如PMS订单的 `dept_id` 将关联到 `sys_dept.dept_id``create_by` 将关联到 `sys_user.user_id`。部门进一步通过 `sys_dept.tenant_id` 关联到租户。**
### 3.2 PMS核心业务表
#### 3.2.1 `pms_room_types` - 房型表
* 业务描述: 存储各部门(门店)定义的房型信息。
* 主键: `room_type_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `room_type_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '房型唯一ID'): 主键。
* `dept_id` (BIGINT(20), NOT NULL, COMMENT '部门ID(门店), 关联 sys_dept.dept_id')。
* `name` (VARCHAR(255), NOT NULL, COMMENT '房型名称 (例如: 豪华大床房)')。
* `default_price` (DECIMAL(10,2), NOT NULL, COMMENT '房型默认价格 (每晚)')。
* `capacity` (INT, NOT NULL, COMMENT '标准入住人数')。
* `amenities` (JSON, NULLABLE, COMMENT '房型设施标签。JSON数组例如 [\"wifi\", \"空调\"]')。
* `description` (TEXT, NULLABLE, COMMENT '房型描述')。
* `images_json` (JSON, NULLABLE, COMMENT '房型图片。JSON数组')。
* `sort_order` (INT, DEFAULT 0, COMMENT '显示排序值')。
* `status` (VARCHAR(50), NOT NULL, DEFAULT 'active', COMMENT '状态。枚举: active (启用), inactive (禁用)')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 索引: `idx_pms_rt_dept_name` (dept_id, name), `idx_pms_rt_dept_status` (dept_id, status)
#### 3.2.2 `pms_room_rooms` - 房间表
* 业务描述: 存储各部门(门店)的具体房间信息。
* 主键: `room_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `room_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '房间唯一ID')。
* `dept_id` (BIGINT(20), NOT NULL, COMMENT '部门ID(门店), 关联 sys_dept.dept_id')。
* `room_type_id` (BIGINT, FK, NOT NULL, COMMENT '所属房型ID, 关联 pms_room_types.room_type_id')。
* `room_number` (VARCHAR(50), NOT NULL, COMMENT '房间号 (在门店内应唯一)')。
* `floor` (VARCHAR(50), NULLABLE, COMMENT '楼层')。
* `room_status` (VARCHAR(50), NOT NULL, DEFAULT 'available', COMMENT '物理状态。枚举: available (可用), occupied (占用中), maintenance (维护中), out_of_service (停用服务)')。
* `cleaning_status` (VARCHAR(50), NOT NULL, DEFAULT 'clean', COMMENT '清洁状态。枚举: clean (已清洁), dirty (待清洁), cleaning_in_progress (清洁中), inspected (已查房)')。
* `description` (TEXT, NULLABLE, COMMENT '房间描述或备注')。
* `status` (VARCHAR(50), NOT NULL, DEFAULT 'active', COMMENT '记录状态。枚举: active (启用), inactive (禁用)')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 唯一约束: `unq_pms_r_dept_room_number` (dept_id, room_number, del_flag)
* 索引: `idx_pms_r_dept_rt` (dept_id, room_type_id), `idx_pms_r_dept_room_status` (dept_id, room_status), `idx_pms_r_dept_cleaning_status` (dept_id, cleaning_status)
#### 3.2.3 `pms_room_locks` - 房间锁定记录表
* 业务描述: 记录房间因维护、手动操作等原因被锁定的情况。
* 主键: `lock_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `lock_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '锁定记录唯一ID')。
* `dept_id` (BIGINT(20), NOT NULL, COMMENT '部门ID, 关联 sys_dept.dept_id')。
* `room_id` (BIGINT, FK, NOT NULL, COMMENT '被锁定的房间ID, 关联 pms_room_rooms.room_id')。
* `start_datetime` (DATETIME, NOT NULL, COMMENT '锁定开始时间')。
* `end_datetime` (DATETIME, NOT NULL, COMMENT '锁定结束时间')。
* `reason` (TEXT, NULLABLE, COMMENT '锁定原因')。
* `lock_type` (VARCHAR(50), NOT NULL, DEFAULT 'manual_lock', COMMENT '锁定类型。枚举: manual_lock (手动锁定), maintenance (维护), auto_block (自动锁定), staff_use (员工自用)')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 索引: `idx_pms_rl_tenant_dept_room_time` (tenant_id, dept_id, room_id, start_datetime, end_datetime), `idx_pms_rl_tenant_dept_lock_type` (tenant_id, dept_id, lock_type)
#### 3.2.4 `pms_core_orders` - 核心订单表
* 业务描述: 存储所有来源的预订订单核心信息。
* 主键: `order_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `order_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '订单唯一ID')。
* `dept_id` (BIGINT(20), NOT NULL, COMMENT '部门ID, 关联 sys_dept.dept_id')。
* `contact_id` (BIGINT, FK, NULLABLE, COMMENT '主要联系人ID, 关联 cmn_contacts.contact_id')。
* `primary_contact_name` (VARCHAR(100), NOT NULL, COMMENT '主要联系人姓名 (冗余)')。
* `primary_contact_phone` (VARCHAR(50), NOT NULL, COMMENT '主要联系人电话 (冗余)')。
* `pms_room_id` (BIGINT, FK, NULLABLE, COMMENT '分配的房间ID, 关联 pms_room_rooms.room_id, 入住时分配')。
* `room_type_id` (BIGINT, FK, NOT NULL, COMMENT '预订的房型ID, 关联 pms_room_types.room_type_id')。
* `channel_id` (BIGINT, FK, NULLABLE, COMMENT '订单来源渠道ID, 关联 pms_core_channels.channel_id')。
* `check_in_date` (DATE, NOT NULL, COMMENT '计划入住日期')。
* `check_out_date` (DATE, NOT NULL, COMMENT '计划离店日期')。
* `num_adults` (INT, NOT NULL, DEFAULT 1, COMMENT '成人数')。
* `num_children` (INT, DEFAULT 0, COMMENT '儿童数')。
* `estimated_arrival_time` (TIME, NULLABLE, COMMENT '预计抵达时间')。
* `total_amount` (DECIMAL(10,2), NOT NULL, COMMENT '订单总金额')。
* `paid_amount` (DECIMAL(10,2), DEFAULT 0.00, COMMENT '已付金额')。
* `due_amount` (DECIMAL(10,2), AS (`total_amount` - `paid_amount`)) STORED COMMENT '应付金额 (计算列)'。
* `currency` (VARCHAR(3), NOT NULL, DEFAULT 'CNY', COMMENT '货币代码')。
* `order_status` (VARCHAR(50), NOT NULL, COMMENT '订单状态。枚举: pending_confirmation, confirmed, checked_in, checked_out, cancelled, no_show, extended, waitlist')。
* `order_source` (VARCHAR(50), NOT NULL, COMMENT '订单来源。枚举: direct_walk_in, direct_phone, direct_website, direct_mall_h5, ota_channel_manager, travel_agency, corporate, other')。
* `notes` (TEXT, NULLABLE, COMMENT '订单备注')。
* `cancelled_at` (DATETIME, NULLABLE, COMMENT '取消时间')。
* `cancellation_reason` (TEXT, NULLABLE, COMMENT '取消原因')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 索引: `idx_pms_o_tenant_dept_status_dates` (tenant_id, dept_id, order_status, check_in_date, check_out_date), `idx_pms_o_tenant_dept_contact_phone` (tenant_id, dept_id, primary_contact_phone), `idx_pms_o_tenant_dept_source` (tenant_id, dept_id, order_source)
#### 3.2.5 `pms_core_order_items` - 核心订单项目表
* 业务描述: 记录订单中包含的具体商品或服务项目。
* 主键: `order_item_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `order_item_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '订单项目唯一ID')。
* `order_id` (BIGINT, FK, NOT NULL, COMMENT '所属订单ID, 关联 pms_core_orders.order_id')。
* `dept_id` (BIGINT(20), NOT NULL, COMMENT '部门ID, 关联 sys_dept.dept_id (冗余)')。
* `pms_room_id` (BIGINT, FK, NULLABLE, COMMENT '关联房间ID (如房晚项目), 关联 pms_room_rooms.room_id')。
* `product_id` (BIGINT, FK, NULLABLE, COMMENT '关联产品ID (多态), 如 pms_finance_extra_charge_items.item_id')。
* `product_type` (VARCHAR(50), NOT NULL, COMMENT '产品类型。枚举: room_night (房晚), extra_charge_item (附加收费项目), package_component (套餐子项), service_fee (服务费), discount_adjustment (折扣调整)')。
* `description` (VARCHAR(255), NOT NULL, COMMENT '项目描述 (例如: 豪华大床房住宿, 额外早餐)')。
* `quantity` (INT, NOT NULL, DEFAULT 1, COMMENT '数量')。
* `unit_price` (DECIMAL(10,2), NOT NULL, COMMENT '单价')。
* `total_price` (DECIMAL(10,2), AS (`quantity` * `unit_price`)) STORED COMMENT '总价 (计算列)'。
* `service_date` (DATE, NULLABLE, COMMENT '服务发生日期 (例如房晚对应的日期)')。
* `notes` (TEXT, NULLABLE, COMMENT '项目备注')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 索引: `idx_pms_oi_order_id` (order_id), `idx_pms_oi_tenant_dept_product_type` (tenant_id, dept_id, product_type)
#### 3.2.6 `pms_core_channels` - 订单来源渠道表
* 业务描述: 管理订单的来源渠道。
* 主键: `channel_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `channel_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '渠道唯一ID')。
* `dept_id` (BIGINT(20), NULLABLE, COMMENT '部门ID, 关联 sys_dept.dept_id, 若渠道归属特定部门')。
* `name` (VARCHAR(100), NOT NULL, COMMENT '渠道名称 (例如: 携程, 官网直订)')。
* `channel_type` (VARCHAR(50), NOT NULL, COMMENT '渠道类型。枚举: ota, direct_booking (官网/电话/前台), gds (全球分销系统), wholesaler (批发商), corporate (公司协议), internal_use (内部使用), other')。
* `description` (TEXT, NULLABLE, COMMENT '渠道描述')。
* `status` (VARCHAR(50), NOT NULL, DEFAULT 'active', COMMENT '状态。枚举: active (激活), inactive (停用), deprecated (弃用)')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 索引: `idx_pms_ch_tenant_dept_name` (tenant_id, dept_id, name), `idx_pms_ch_type` (channel_type), `idx_pms_ch_status` (status)
#### 3.2.7 `pms_finance_folios` - 客户账单(Folio)表
* 业务描述: 记录与订单关联的客户账单核心信息。
* 主键: `folio_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `folio_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '账单唯一ID')。
* `dept_id` (BIGINT(20), NOT NULL, COMMENT '部门ID, 关联 sys_dept.dept_id')。
* `order_id` (BIGINT, UNIQUE, FK, NOT NULL, COMMENT '关联的订单ID, 关联 pms_core_orders.order_id')。
* `total_charges` (DECIMAL(10,2), NOT NULL, DEFAULT 0.00, COMMENT '总应收费用')。
* `total_payments` (DECIMAL(10,2), NOT NULL, DEFAULT 0.00, COMMENT '总已收付款')。
* `total_refunds` (DECIMAL(10,2), NOT NULL, DEFAULT 0.00, COMMENT '总已退款')。
* `balance` (DECIMAL(10,2), AS (`total_charges` - `total_payments` + `total_refunds`)) STORED COMMENT '当前余额 (计算列)'。
* `folio_status` (VARCHAR(50), NOT NULL, DEFAULT 'open', COMMENT '账单状态。枚举: open (开放/未结清), closed (已结清/关闭), void (作废), pending_settlement (待结算)')。
* `notes` (TEXT, NULLABLE, COMMENT '账单备注')。
* `closed_at` (DATETIME, NULLABLE, COMMENT '账单关闭时间')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 索引: `idx_pms_f_tenant_dept_order_id` (tenant_id, dept_id, order_id), `idx_pms_f_tenant_dept_status` (tenant_id, dept_id, folio_status)
#### 3.2.8 `pms_finance_transactions` - 财务交易流水表
* 业务描述: 记录在Folio中的每一笔财务交易。
* 主键: `transaction_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `transaction_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '交易唯一ID')。
* `folio_id` (BIGINT, FK, NOT NULL, COMMENT '所属账单ID, 关联 pms_finance_folios.folio_id')。
* `dept_id` (BIGINT(20), NOT NULL, COMMENT '部门ID, 关联 sys_dept.dept_id (冗余)')。
* `transaction_type` (VARCHAR(50), NOT NULL, COMMENT '交易类型。枚举: charge (应收费用), payment (客人付款), refund (退款给客人), deposit (押金), deposit_refund (押金退还), adjustment_positive (正调整), adjustment_negative (负调整)')。
* `amount` (DECIMAL(10,2), NOT NULL, COMMENT '交易金额')。
* `description` (VARCHAR(255), NOT NULL, COMMENT '交易描述')。
* `payment_method_id` (BIGINT, FK, NULLABLE, COMMENT '支付方式ID, 关联 pms_finance_payment_methods.payment_method_id')。
* `payment_gateway_txn_id` (VARCHAR(255), NULLABLE, COMMENT '支付网关交易号')。
* `transaction_time` (DATETIME, NOT NULL, COMMENT '交易实际发生时间')。
* `related_order_item_id` (BIGINT, FK, NULLABLE, COMMENT '关联的订单项ID, 关联 pms_core_order_items.order_item_id')。
* `notes` (TEXT, NULLABLE, COMMENT '交易备注')。
* `is_void` (BOOLEAN, NOT NULL, DEFAULT FALSE, COMMENT '是否已作废冲销')。
* `voided_at` (DATETIME, NULLABLE, COMMENT '作废时间')。
* `voided_reason` (TEXT, NULLABLE, COMMENT '作废原因')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 索引: `idx_pms_ft_folio_id` (folio_id), `idx_pms_ft_tenant_dept_type_time` (tenant_id, dept_id, transaction_type, transaction_time), `idx_pms_ft_payment_gateway_txn_id` (payment_gateway_txn_id)
#### 3.2.9 `pms_finance_payment_methods` - 支付方式表
* 业务描述: 存储租户可定义的支付方式。
* 主键: `payment_method_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `payment_method_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '支付方式唯一ID')。
* `dept_id` (BIGINT(20), NULLABLE, COMMENT '部门ID, 关联 sys_dept.dept_id, 若支付方式归属特定部门')。
* `name` (VARCHAR(100), NOT NULL, COMMENT '支付方式名称 (例如: 现金, 微信支付)')。
* `method_type` (VARCHAR(50), NOT NULL, COMMENT '支付方式类型。枚举: cash, credit_card_visa, credit_card_mastercard, credit_card_amex, alipay, wechat_pay, bank_transfer, company_account, voucher, points, other_online_payment, mobile_payment, other')。
* `details_json` (JSON, NULLABLE, COMMENT '支付方式附加配置。例如支付网关参数等')。
* `status` (VARCHAR(50), NOT NULL, DEFAULT 'active', COMMENT '状态。枚举: active (激活), inactive (停用)')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 索引: `idx_pms_fpm_tenant_dept_name` (tenant_id, dept_id, name), `idx_pms_fpm_tenant_dept_type` (tenant_id, dept_id, method_type), `idx_pms_fpm_tenant_dept_status` (tenant_id, dept_id, status)
#### 3.2.10 `pms_finance_extra_charge_items` - 附加费用项目表
* 业务描述: 存储租户可预定义的附加消费项目,如额外早餐、加床费等。
* 主键: `item_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `item_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '附加费用项目唯一ID')。
* `dept_id` (BIGINT(20), NULLABLE, COMMENT '部门ID, 关联 sys_dept.dept_id, 若项目归属特定部门')。
* `name` (VARCHAR(255), NOT NULL, COMMENT '项目名称')。
* `default_price` (DECIMAL(10,2), NOT NULL, COMMENT '默认单价')。
* `category` (VARCHAR(100), NULLABLE, COMMENT '费用类别,如餐饮、服务、商品等')。
* `is_taxable` (BOOLEAN, NOT NULL, DEFAULT TRUE, COMMENT '是否应税')。
* `description` (TEXT, NULLABLE, COMMENT '项目描述')。
* `status` (VARCHAR(50), NOT NULL, DEFAULT 'active', COMMENT '状态。枚举: active (激活), inactive (停用)')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 索引: `idx_pms_feci_tenant_dept_name` (tenant_id, dept_id, name), `idx_pms_feci_tenant_dept_category` (tenant_id, dept_id, category), `idx_pms_feci_tenant_dept_status` (tenant_id, dept_id, status)
#### 3.2.11 `pms_room_pricing_rules` - 房间价格规则表
* 业务描述: 为特定房型在特定日期范围或特定条件下设置价格调整规则。
* 主键: `rule_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `rule_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '价格规则唯一ID')。
* `dept_id` (BIGINT(20), NULLABLE, COMMENT '部门ID, 关联 sys_dept.dept_id, 若规则归属特定部门')。
* `name` (VARCHAR(255), NOT NULL, COMMENT '规则名称 (例如: 周末特价, 连住优惠)')。
* `room_type_id` (BIGINT, FK, NULLABLE, COMMENT '适用的房型ID, 关联 pms_room_types.room_type_id, NULL表示适用于所有房型')。
* `date_range_start` (DATE, NULLABLE, COMMENT '规则适用开始日期')。
* `date_range_end` (DATE, NULLABLE, COMMENT '规则适用结束日期')。
* `days_of_week_json` (JSON, NULLABLE, COMMENT 'JSON数组数字代表星期几, e.g., [1,2,7] (1-Mon, 7-Sun)')。
* `min_length_of_stay` (INT, NULLABLE, COMMENT '最小入住天数要求')。
* `max_length_of_stay` (INT, NULLABLE, COMMENT '最大入住天数限制')。
* `price_adjustment_type` (VARCHAR(50), NOT NULL, COMMENT '调整类型。枚举: fixed_amount_override (固定价格覆盖), percentage_discount_from_base (基价百分比折扣), fixed_amount_increase_on_base (基价固定金额上浮), fixed_amount_decrease_from_base (基价固定金额下调), set_to_value (设置为特定值)')。
* `adjustment_value` (DECIMAL(10,2), NOT NULL, COMMENT '调整值 (具体金额或百分比如50.00或0.1代表10%)')。
* `priority` (INT, NOT NULL, DEFAULT 0, COMMENT '规则优先级,数字越大优先级越高')。
* `status` (VARCHAR(50), NOT NULL, DEFAULT 'active', COMMENT '状态。枚举: active (激活), inactive (停用), scheduled (计划中)')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 索引: `idx_pms_rpr_tenant_dept_dates` (tenant_id, dept_id, date_range_start, date_range_end), `idx_pms_rpr_tenant_dept_status_priority` (tenant_id, dept_id, status, priority)
#### 3.2.12 `pms_tenant_user_devices` - 租户用户设备表
* 业务描述: 用于存储租户用户(员工)登录过的设备信息,主要用于推送通知。
* 主键: `id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '设备记录唯一ID')。
* `user_id` (BIGINT(20), FK, NOT NULL, COMMENT '关联的用户ID, 关联 sys_user.user_id')。
* `device_type` (VARCHAR(50), NOT NULL, COMMENT '设备类型,例如: ios_app, android_app, wechat_miniprogram, web_push_browser')。
* `device_token` (VARCHAR(512), NOT NULL, COMMENT '设备推送令牌')。
* `app_version` (VARCHAR(50), NULLABLE, COMMENT '客户端应用版本')。
* `last_login_at` (DATETIME, NOT NULL, COMMENT '此设备最后登录时间')。
* `status` (VARCHAR(50), NOT NULL, DEFAULT 'active', COMMENT '状态。枚举: active (激活), inactive (停用), token_expired (令牌过期)')。
* `create_time` (DATETIME, NOT NULL, COMMENT '记录创建时间')。
* `update_time` (DATETIME, NOT NULL, COMMENT '记录更新时间')。
* 唯一约束: `unq_pms_ud_user_device_token_type` (user_id, device_token, device_type)
* 索引: `idx_pms_ud_tenant_user_token` (tenant_id, user_id, device_token), `idx_pms_ud_user_status` (user_id, status)
#### 3.2.13 `pms_tenant_settings` - 租户级PMS特定设置表
* 业务描述: 存储租户或部门级别的PMS特定业务配置项。与若依的 `sys_config` 互补。
* 主键: `setting_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `setting_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '设置唯一ID')。
* `tenant_id` (VARCHAR(20), NOT NULL, COMMENT '租户编号, 关联 sys_tenant.tenant_id')。
* `dept_id` (BIGINT(20), NULLABLE, COMMENT '部门ID, 关联 sys_dept.dept_id, NULL表示租户全局PMS设置')。
* `setting_group` (VARCHAR(100), NOT NULL, COMMENT '设置分组,例如: pms_booking_rules, pms_financial_params, pms_ui_appearance')。
* `setting_key` (VARCHAR(100), NOT NULL, COMMENT '设置项的唯一键 (在tenant_id, dept_id, setting_group下唯一)')。
* `setting_value` (TEXT, NOT NULL, COMMENT '设置项的值 (根据需要可存储JSON字符串)')。
* `value_type` (VARCHAR(50), NOT NULL, DEFAULT 'string', COMMENT '值类型,如 string, integer, boolean, json方便解析')。
* `description` (TEXT, NULLABLE, COMMENT '设置项描述')。
* `is_sensitive` (BOOLEAN, NOT NULL, DEFAULT FALSE, COMMENT '是否为敏感设置')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 唯一约束: `unq_pms_ts_tenant_dept_group_key` (tenant_id, dept_id, setting_group, setting_key, del_flag)
* 索引: `idx_pms_ts_tenant_dept_group_key` (tenant_id, dept_id, setting_group, setting_key)
#### 3.2.14 `pms_mp_settings` - 民宿管理小程序配置表
* 业务描述: 存储租户或部门级别针对民宿员工管理小程序的特定配置项。
* 主键: `setting_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `setting_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '小程序设置唯一ID')。
* `tenant_id` (VARCHAR(20), NOT NULL, COMMENT '租户编号, 关联 sys_tenant.tenant_id')。
* `dept_id` (BIGINT(20), NULLABLE, COMMENT '部门ID, 关联 sys_dept.dept_id, NULL表示租户全局小程序设置')。
* `setting_key` (VARCHAR(100), NOT NULL, COMMENT '小程序配置项的唯一键。例如: mp_theme_color, mp_enable_feature_xyz')。
* `setting_value` (TEXT, NOT NULL, COMMENT '配置项的值 (可存储JSON字符串)')。
* `value_type` (VARCHAR(50), NOT NULL, DEFAULT 'string', COMMENT '值类型,如 string, integer, boolean, json')。
* `description` (TEXT, NULLABLE, COMMENT '配置项描述')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 唯一约束: `unq_pms_mps_tenant_dept_key` (tenant_id, dept_id, setting_key, del_flag)
* 索引: `idx_pms_mps_tenant_dept_key` (tenant_id, dept_id, setting_key)
### 3.3 公共数据模型 (PMS相关)
#### 3.3.1 `cmn_contacts` - 联系人表
* 业务描述: 存储系统中所有类型的联系人信息主要是PMS的住客。
* 主键: `contact_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `contact_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '联系人唯一ID')。
* `dept_id` (BIGINT(20), NULLABLE, COMMENT '部门ID, 关联 sys_dept.dept_id, 若联系人主要归属某部门')。
* `contact_type` (VARCHAR(50), NOT NULL, COMMENT '联系人类型。枚举: guest_individual (散客), guest_group_contact (团队联系人), corporate_contact (公司协议联系人), travel_agent_contact (旅行社联系人), company_profile (公司档案), supplier_contact (供应商联系人), employee_profile (员工档案关联), other (其他)')。
* `full_name` (VARCHAR(255), NULLABLE, COMMENT '姓名')。
* `phone_number` (VARCHAR(50), NULLABLE, COMMENT '主要联系电话')。
* `email` (VARCHAR(255), NULLABLE, COMMENT '电子邮件地址')。
* `wechat_openid` (VARCHAR(100), NULLABLE, COMMENT '微信OpenID (特定于单个微信公众号或小程序)')。
* `wechat_unionid` (VARCHAR(100), NULLABLE, COMMENT '微信UnionID (跨多个微信应用的用户唯一标识)')。
* `related_user_id` (BIGINT(20), FK, NULLABLE, COMMENT '关联的系统用户ID, 关联 sys_user.user_id, 如员工档案关联')。
* `gender` (VARCHAR(20), NULLABLE, COMMENT '性别。枚举: male (男), female (女), non_binary (非二元), prefer_not_to_say (不愿透露), unknown (未知)')。
* `date_of_birth` (DATE, NULLABLE, COMMENT '出生日期')。
* `id_type` (VARCHAR(50), NULLABLE, COMMENT '证件类型如身份证、护照等。可关联sys_dict_data')。
* `id_number_encrypted` (VARCHAR(255), NULLABLE, COMMENT '证件号码 (加密存储)')。
* `nationality_country_code` (VARCHAR(10), NULLABLE, COMMENT '国籍代码 (ISO 3166-1 alpha-2)')。
* `preferred_language` (VARCHAR(10), NULLABLE, COMMENT '偏好语言代码 (e.g., en, zh-CN)')。
* `address_street` (VARCHAR(255), NULLABLE, COMMENT '街道地址')。
* `address_city` (VARCHAR(100), NULLABLE, COMMENT '城市')。
* `address_state_province` (VARCHAR(100), NULLABLE, COMMENT '省/州')。
* `address_postal_code` (VARCHAR(50), NULLABLE, COMMENT '邮政编码')。
* `address_country_code` (VARCHAR(10), NULLABLE, COMMENT '国家代码 (ISO 3166-1 alpha-2)')。
* `contact_status` (VARCHAR(50), NOT NULL, DEFAULT 'active', COMMENT '联系人状态。枚举: active (活跃), inactive (不活跃), prospect (潜在客户), blacklisted (黑名单), pending_verification (待验证), merged_duplicate (已合并重复)')。
* `remarks` (TEXT, NULLABLE, COMMENT '备注信息')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 唯一约束 (建议,考虑 `del_flag`):
* `unq_cmn_c_tenant_dept_phone` (tenant_id, dept_id, phone_number, del_flag) (如果电话在部门内唯一)
* `unq_cmn_c_tenant_dept_email` (tenant_id, dept_id, email, del_flag) (如果邮箱在部门内唯一)
* 索引: `idx_cmn_c_tenant_dept_fname` (tenant_id, dept_id, full_name), `idx_cmn_c_tenant_dept_status_type` (tenant_id, dept_id, contact_status, contact_type), `idx_cmn_c_related_user` (related_user_id)
#### 3.3.2 `cmn_contact_tags` - 联系人标签表
* 业务描述: 定义可用于分类和标记联系人的标签。
* 主键: `tag_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `tag_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '标签唯一ID')。
* `dept_id` (BIGINT(20), NULLABLE, COMMENT '部门ID, 关联 sys_dept.dept_id, 若标签归属特定部门或全局')。
* `name` (VARCHAR(100), NOT NULL, COMMENT '标签名称')。
* `color` (VARCHAR(20), NULLABLE, COMMENT '标签显示颜色,如十六进制色值 #FF5733')。
* `category` (VARCHAR(50), NULLABLE, COMMENT '标签分类,如兴趣、客户级别、来源等')。
* `description` (TEXT, NULLABLE, COMMENT '标签描述')。
* `is_system` (BOOLEAN, NOT NULL, DEFAULT FALSE, COMMENT '是否为系统预设标签 (不可删除)')。
* `sort_order` (INT, NOT NULL, DEFAULT 0, COMMENT '排序值')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NULLABLE, COMMENT '创建时间')。
* `update_by` (BIGINT(20), NULLABLE, COMMENT '更新者ID, 关联 sys_user.user_id')。
* `update_time` (DATETIME, NULLABLE, COMMENT '更新时间')。
* `create_dept_id` (BIGINT(20), NULLABLE, COMMENT '创建部门ID, 关联 sys_dept.dept_id')。
* `del_flag` (CHAR(1), NOT NULL, DEFAULT '0', COMMENT '删除标志0存在 1删除')。
* 唯一约束: `unq_cmn_ct_tenant_dept_name_category` (tenant_id, dept_id, name, category, del_flag)
* 索引: `idx_cmn_ct_category` (category), `idx_cmn_ct_is_system` (is_system)
#### 3.3.3 `cmn_contact_tag_relations` - 联系人标签关联表
* 业务描述: 实现联系人与标签的多对多关系。
* 主键: `relation_id` (BIGINT AUTO_INCREMENT)
* 字段定义:
* `relation_id` (BIGINT, PK, AUTO_INCREMENT, NOT NULL, COMMENT '关联唯一ID')。
* `dept_id` (BIGINT(20), NOT NULL, COMMENT '部门ID, 关联 sys_dept.dept_id (冗余, 以联系人所属部门为准)')。
* `contact_id` (BIGINT, FK, NOT NULL, COMMENT '联系人ID, 关联 cmn_contacts.contact_id')。
* `tag_id` (BIGINT, FK, NOT NULL, COMMENT '标签ID, 关联 cmn_contact_tags.tag_id')。
* `create_by` (BIGINT(20), NULLABLE, COMMENT '创建者ID, 关联 sys_user.user_id')。
* `create_time` (DATETIME, NOT NULL, COMMENT '创建时间')。
* 唯一约束: `unq_cmn_ctr_tenant_dept_contact_tag` (tenant_id, dept_id, contact_id, tag_id)
* 索引: `idx_cmn_ctr_contact_id` (contact_id), `idx_cmn_ctr_tag_id` (tag_id)
## 4. 主要业务实体状态流转
#### 4.1 订单 (`pms_core_orders`) 状态流转
```mermaid
stateDiagram-v2
[*] --> pending_confirmation: 创建订单
pending_confirmation --> confirmed: 确认订单
pending_confirmation --> cancelled: 取消订单
confirmed --> checked_in: 办理入住
confirmed --> cancelled: 取消订单
confirmed --> no_show: 未按时入住
checked_in --> checked_out: 办理退房
checked_in --> extended: 延长入住
extended --> checked_out: 办理退房
checked_out --> [*] : 订单完成
cancelled --> [*] : 订单取消
no_show --> [*] : 订单标记为No Show
```
* **订单状态 (`order_status` VARCHAR(50)) 枚举值示例:**
* `pending_confirmation` (待确认)
* `confirmed` (已确认)
* `checked_in` (已入住)
* `checked_out` (已退房)
* `cancelled` (已取消)
* `no_show` (未到店)
* `extended` (已延期)
* `waitlist` (等候名单)
#### 4.2 房间 (`pms_room_rooms`) 状态流转 (物理状态 & 清洁状态)
**物理状态 (`room_status` VARCHAR(50))**
```mermaid
stateDiagram-v2
[*] --> available: 房间空闲可用
available --> occupied: 客人入住 / 预留占用
available --> maintenance: 安排维护 / 维修
available --> out_of_service: 暂停服务 (其他原因)
occupied --> available: 客人退房 (且房间已清洁完成)
maintenance --> available: 维护完成 (房间恢复可用)
out_of_service --> available: 恢复服务
```
* **物理状态枚举值示例:**
* `available` (可用)
* `occupied` (占用中)
* `maintenance` (维护中)
* `out_of_service` (暂停服务)
**清洁状态 (`cleaning_status` VARCHAR(50))**
```mermaid
stateDiagram-v2
[*] --> clean: 初始状态 / 清洁完成
clean --> dirty: 客人使用后 / 退房后标记
dirty --> cleaning_in_progress: 客房服务员开始清洁
cleaning_in_progress --> inspected: 清洁完成待查房
inspected --> clean: 查房通过
inspected --> cleaning_in_progress: 查房未通过,返工
clean --> cleaning_in_progress: 客中打扫 (客人仍在住,但安排了打扫)
```
* **清洁状态枚举值示例:**
* `clean` (已清洁)
* `dirty` (待清洁)
* `cleaning_in_progress` (清洁中)
* `inspected` (已查房/待检)
#### 4.3 账单 (`pms_finance_folios`) 状态流转
```mermaid
stateDiagram-v2
[*] --> open: 订单创建时自动生成 / 手动创建
open --> closed: 账务结清 (所有费用付清)
open --> void: 作废账单 (如错误创建)
open --> pending_settlement: 等待结算 (如对公转账)
pending_settlement --> closed: 结算完成
pending_settlement --> open: 取消结算,返回开放
closed --> open: 重新打开 (特殊情况,需权限,如补充入账或退款)
void --> [*] : 账单作废
closed --> [*] : 账单关闭
```
* **账单状态 (`folio_status` VARCHAR(50)) 枚举值示例:**
* `open` (开放/未结清)
* `closed` (已结清/关闭)
* `void` (作废)
* `pending_settlement` (待结算)
## 5. 核心业务枚举值定义 (PMS主要部分)
以下列出PMS核心业务表中 `VARCHAR(50)` 类型枚举字段的建议值,具体值可根据业务细化调整。
* **`pms_room_types.status`**: `active` (启用), `inactive` (禁用)
* **`pms_room_rooms.room_status`**: `available` (可用), `occupied` (占用中), `maintenance` (维护中), `out_of_service` (暂停服务)
* **`pms_room_rooms.cleaning_status`**: `clean` (已清洁), `dirty` (待清洁), `cleaning_in_progress` (清洁中), `inspected` (已查房/待检)
* **`pms_room_rooms.status` (记录状态)**: `active` (启用), `inactive` (禁用)
* **`pms_room_locks.lock_type`**: `manual_lock` (手动锁定), `maintenance` (维护), `auto_block` (自动锁定,如欠费), `staff_use` (员工自用), `cleaning_block` (清洁锁定)
* **`pms_core_orders.order_status`**: `pending_confirmation` (待确认), `confirmed` (已确认), `checked_in` (已入住), `checked_out` (已退房), `cancelled` (已取消), `no_show` (未到店), `extended` (已延期), `waitlist` (等候名单), `provisional` (暂定预订)
* **`pms_core_orders.order_source`**: `direct_walk_in` (现场步入), `direct_phone` (电话预订), `direct_website` (官网预订), `direct_mall_h5` (H5商城), `ota_channel_manager` (OTA渠道管理), `travel_agency` (旅行社), `corporate_account` (公司协议), `internal_booking` (内部预订), `other` (其他)
* **`pms_core_order_items.product_type`**: `room_night` (房晚), `extra_charge_item` (附加收费项目), `package_component` (套餐子项), `service_fee` (服务费), `discount_adjustment` (折扣调整), `cancellation_fee` (取消费)
* **`pms_core_channels.channel_type`**: `ota` (在线旅行社), `direct_booking` (直接预订-官网/电话/前台), `gds` (全球分销系统), `wholesaler` (批发商), `corporate` (公司协议), `internal_use` (内部使用), `meta_search` (元搜索), `other` (其他)
* **`pms_core_channels.status`**: `active` (激活), `inactive` (停用), `deprecated` (弃用)
* **`pms_finance_folios.folio_status`**: `open` (开放/未结清), `closed` (已结清/关闭), `void` (作废), `pending_settlement` (待结算)
* **`pms_finance_transactions.transaction_type`**: `charge` (应收费用), `payment` (客人付款), `refund` (退款给客人), `deposit` (押金收款), `deposit_refund` (押金退还), `adjustment_positive` (正调整), `adjustment_negative` (负调整), `folio_transfer_out` (转出), `folio_transfer_in` (转入)
* **`pms_finance_payment_methods.method_type`**: `cash` (现金), `credit_card_visa` (VISA卡), `credit_card_mastercard` (万事达卡), `credit_card_amex` (运通卡), `credit_card_unionpay` (银联卡), `alipay` (支付宝), `wechat_pay` (微信支付), `bank_transfer` (银行转账), `company_account` (公司账户/挂账), `voucher` (代金券), `points_redemption` (积分兑换), `other_online_payment` (其他在线支付), `mobile_payment` (其他移动支付), `other` (其他)
* **`pms_finance_payment_methods.status`**: `active` (激活), `inactive` (停用)
* **`pms_finance_extra_charge_items.status`**: `active` (激活), `inactive` (停用)
* **`pms_room_pricing_rules.price_adjustment_type`**: `fixed_amount_override` (固定价格覆盖), `percentage_discount_from_base` (基价百分比折扣), `fixed_amount_increase_on_base` (基价固定金额上浮), `fixed_amount_decrease_from_base` (基价固定金额下调), `set_to_value` (设置为特定值)
* **`pms_room_pricing_rules.status`**: `active` (激活), `inactive` (停用), `scheduled` (计划中/待生效)
* **`pms_tenant_user_devices.status`**: `active` (激活), `inactive` (停用), `token_expired` (令牌过期)
* **`cmn_contacts.contact_type`**: `guest_individual` (散客), `guest_group_contact` (团队联系人), `corporate_contact` (公司协议联系人), `travel_agent_contact` (旅行社联系人), `company_profile` (公司档案), `supplier_contact` (供应商联系人), `employee_profile` (员工档案关联), `other` (其他)
* **`cmn_contacts.gender`**: `male` (男), `female` (女), `non_binary` (非二元性别), `prefer_not_to_say` (不愿透露), `unknown` (未知)
* **`cmn_contacts.contact_status`**: `active` (活跃), `inactive` (不活跃), `prospect` (潜在客户), `blacklisted` (黑名单), `pending_verification` (待验证), `merged_duplicate` (已合并重复档案)
## 6. 索引命名与规范建议
* **唯一索引:** `unq_[子系统前缀]_[表名简写]_[字段1]_[字段2]`,例如:`unq_pms_r_tdrn` 代表 `pms_room_rooms` 表上 `tenant_id, dept_id, room_number` 的唯一索引。
* **普通索引:** `idx_[子系统前缀]_[表名简写]_[字段1]_[字段2]`,例如:`idx_pms_o_tds` 代表 `pms_core_orders` 表上 `tenant_id, dept_id, order_status` 的普通索引。
* **租户和部门优先:** 对于所有包含 `tenant_id``dept_id` 的PMS业务表查询相关的索引应优先考虑这两个字段顺序通常为 `(tenant_id, dept_id, ...其他条件字段)`
* **覆盖索引:** 尽量设计覆盖索引以减少回表,提高查询性能。
* **避免过多索引:** 每个表的索引数量不宜过多,以免影响写入性能。
---
**(文档末尾)**
**重要提示:**
* 本文档中的主键类型已统一为 `BIGINT AUTO_INCREMENT` (用于PMS新增业务表) 或遵循若依原有的 `BIGINT` (用于若依系统表)。
* PMS业务表中的枚举字段统一为 `VARCHAR(50)`,这与若依的 `CHAR(1)` 不同在系统集成和前端交互时可能需要在API层或逻辑层进行适配和转换。
* 所有PMS业务表已添加 `dept_id` 字段,用于数据归属和统计。确保所有相关查询和业务逻辑都考虑到 `dept_id`
* 审计字段和软删除标记已尽量对齐若依规范。
请仔细审查此最终方案,确保其满足所有业务需求和技术约束。

328
docs/basic/PMS需求.md Normal file
View File

@ -0,0 +1,328 @@
# 云宿居 PMS 系统 - 核心功能需求 (PMS内部聚焦)
## 1. 引言
### 1.1 文档目的与范围
本文档定义了"云宿居"民宿 Property Management System (PMS) 的核心功能需求,**重点聚焦于PMS租户核心业务子系统以下简称PMS核心模块的内部功能**。它旨在明确该子系统的核心功能范围和关键业务流程。
本文档是PMS核心模块后续详细设计和开发工作的基础。
### 1.2 目标读者
产品经理、架构师、开发工程师、测试工程师。
### 1.4 术语表
| 术语 | 全称 | 描述 |
| ------- | -------------------------- | ------------------------------------------------------------- |
| PMS | Property Management System | 民宿/酒店属性管理系统,管理房间、预订、联系人、账务等核心业务 |
| Folio | Folio | 客户账单,用于记录联系人在店期间所有费用和付款的详细清单 |
| Tenant | Tenant | 租户指使用本SaaS化PMS系统的单个民宿运营方 |
| Dept | Department | 部门,在本系统中表示租户下的门店/分店 |
| Contact | Contact | 联系人,指系统中统一管理的客户信息,包括预订人、实际入住人等 |
### 1.5 优先级定义
* **[P0]**: MVP 阶段必须实现的核心功能。
* **[P1]**: 重要功能MVP 后第一个主要版本包含。
* **[P2]**: 次要或完善性功能,后续迭代实现。
* **[P3]**: 未来规划或可选功能。
## 2. 核心设计原则 (适用于PMS内部)
1. **核心稳固**: 保持PMS核心功能房态、订单、账务、联系人的极简和稳定。
2. **数据为基**: 确保PMS核心数据结构设计合理易于内部功能扩展。
3. **安全隐私**: PMS内部数据处理需考虑数据安全和隐私保护。
4. **门店为中心**: 所有业务数据以门店(部门)为归属单位,支持一个租户下多个门店的业务模型。
## 3. 系统架构概览 (PMS视角)
PMS租户核心业务子系统是整个云宿居平台的核心负责处理民宿日常运营的核心业务流程。
```mermaid
flowchart TD
subgraph PMSCore["PMS核心业务子系统"]
PMS["PMS租户<br>核心业务子系统"]:::coreSystem
end
subgraph ExternalSystems["外部关联系统"]
SaaSPlat[SaaS平台管理]:::system
WCDSMM[商城与商品管理]:::system
end
PMSAdmin[PMS核心后台]:::app
OwnerApp[民宿管理小程序]:::app
UserB([民宿主/前台]):::user
SaaSPlat -- "租户配置" --> PMS
WCDSMM -- "预订数据" --> PMS
PMS -- "房态/价格" --> WCDSMM
PMSAdmin --> PMS
OwnerApp --> PMS
UserB -.-> PMSAdmin
UserB -.-> OwnerApp
classDef system fill:#f0f8ff,stroke:#1890ff,stroke-width:1px
classDef coreSystem fill:#f6ffed,stroke:#52c41a,stroke-width:3px,color:#135200,font-weight:bold
classDef app fill:#d9f7be,stroke:#52c41a,stroke-width:1px
classDef user fill:#fff7e6,stroke:#fa8c16,stroke-width:1px,shape:person
```
## 4. PMS租户核心业务子系统 (PMSTenantCoreSubsystem)
**目标:** 为民宿租户提供稳定、高效的核心运营管理能力。
**主要用户:** 民宿主/管理员、前台员工。
#### 4.2.1 房态管理 [P0]
* **功能:**
* **网格日历视图:** 直观展示房间占用情况(已预订、已入住、清洁中、维护锁定)。
* **今日状态列表:** 按预抵、在住、预离分类展示当日订单/房间。
* **清洁状态管理:** 手动标记房间清洁状态 (待清洁/清洁中/已干净)。
* **快捷操作:** 在房态图上可快速创建预订、登记入住/退房、标记房间清洁状态、设置房间临时锁定/解锁。支持拖动修改预订的入住日期或房间。
* **关键数据:**
* `pms_room_types` (room_type_id:BIGINT:pk, dept_id:BIGINT, name:VARCHAR(255), default_price:DECIMAL(10,2), capacity:INT, amenities:JSON, description:TEXT, images_json:JSON, status:VARCHAR(50))
* `pms_room_rooms` (room_id:BIGINT:pk, dept_id:BIGINT, room_type_id:BIGINT, room_number:VARCHAR(50), floor:VARCHAR(50), room_status:VARCHAR(50), cleaning_status:VARCHAR(50), description:TEXT, status:VARCHAR(50))
* `pms_room_locks` (lock_id:BIGINT:pk, dept_id:BIGINT, room_id:BIGINT, start_datetime:DATETIME, end_datetime:DATETIME, reason:TEXT, lock_type:VARCHAR(50))
#### 4.2.2 订单管理 [P0, P1, P2]
* **目标:** 高效、准确地管理所有来源的预订订单。
* **[P0] 核心订单生命周期管理:**
* 功能: 手动创建订单、接收外部系统订单、订单详情查看与编辑、核心状态管理(待确认、已确认、已入住、已退房、已取消、未入住)、关联联系人与房间、生成账单基础、房态联动。
* 库存策略: `pending_confirmation` 状态不锁硬库存。`confirmed` 状态锁定库存。
* 关键数据:
* `pms_core_orders` (order_id:BIGINT:pk, dept_id:BIGINT, contact_id:BIGINT, primary_contact_name:VARCHAR(100), primary_contact_phone:VARCHAR(50), pms_room_id:BIGINT, room_type_id:BIGINT, channel_id:BIGINT, check_in_date:DATE, check_out_date:DATE, num_adults:INT, num_children:INT, estimated_arrival_time:TIME, total_amount:DECIMAL(10,2), paid_amount:DECIMAL(10,2), due_amount:DECIMAL(10,2), currency:VARCHAR(3), order_status:VARCHAR(50), order_source:VARCHAR(50), notes:TEXT, cancelled_at:DATETIME, cancellation_reason:TEXT)
* `pms_core_order_items` (order_item_id:BIGINT:pk, order_id:BIGINT, dept_id:BIGINT, pms_room_id:BIGINT, product_id:BIGINT, product_type:VARCHAR(50), description:VARCHAR(255), quantity:INT, unit_price:DECIMAL(10,2), total_price:DECIMAL(10,2), service_date:DATE, notes:TEXT)
* `pms_core_channels` (channel_id:BIGINT:pk, dept_id:BIGINT, name:VARCHAR(100), channel_type:VARCHAR(50), description:TEXT, status:VARCHAR(50))
* 状态流转图:
```mermaid
stateDiagram-v2
[*] --> pending_confirmation: 创建订单
pending_confirmation --> confirmed: 确认订单
pending_confirmation --> cancelled: 取消订单
confirmed --> checked_in: 办理入住
confirmed --> cancelled: 取消订单
confirmed --> no_show: 未按时入住
checked_in --> checked_out: 办理退房
checked_in --> extended: 延长入住
extended --> checked_out: 办理退房
checked_out --> [*] : 订单完成
cancelled --> [*] : 订单取消
no_show --> [*] : 订单标记为No Show
```
* **[P1] 增强功能:** 订单修改(日期、房型等)、款项管理(预付款、押金)、取消与退款细则、基础支付记录(如手动录入)、自动化通知(预订成功、入住提醒)、订单备注与标签。
* **[P2] 实用订单工具:** 简版团体预订、等候名单、订单报表、发票管理。
#### 4.2.3 财务与支付 (Folio Management) [P0]
* **功能:** 账单管理(Folio)、交易流水、附加消费管理、收款方式与记录、退款处理、账单生成与查询。
* **关键数据:**
* `pms_finance_folios` (folio_id:BIGINT:pk, dept_id:BIGINT, order_id:BIGINT, total_charges:DECIMAL(10,2), total_payments:DECIMAL(10,2), total_refunds:DECIMAL(10,2), balance:DECIMAL(10,2), folio_status:VARCHAR(50), notes:TEXT, closed_at:DATETIME)
* `pms_finance_transactions` (transaction_id:BIGINT:pk, folio_id:BIGINT, dept_id:BIGINT, transaction_type:VARCHAR(50), amount:DECIMAL(10,2), description:VARCHAR(255), payment_method_id:BIGINT, payment_gateway_txn_id:VARCHAR(255), transaction_time:DATETIME, related_order_item_id:BIGINT, notes:TEXT, is_void:BOOLEAN, voided_at:DATETIME, voided_reason:TEXT)
* `pms_finance_payment_methods` (payment_method_id:BIGINT:pk, dept_id:BIGINT, name:VARCHAR(100), method_type:VARCHAR(50), details_json:JSON, status:VARCHAR(50))
* `pms_finance_extra_charge_items` (item_id:BIGINT:pk, dept_id:BIGINT, name:VARCHAR(255), default_price:DECIMAL(10,2), category:VARCHAR(100), is_taxable:BOOLEAN, description:TEXT, status:VARCHAR(50))
#### 4.2.4 联系人与会员管理 [P1] (P0阶段依赖`cmn_contacts`部分字段)
* **功能 (P0涉及):** 订单关联联系人时,使用/创建`cmn_contacts`记录。
* **关键数据 (P0使用部分):**
* `cmn_contacts` (contact_id:BIGINT:pk, dept_id:BIGINT, contact_type:VARCHAR(50), full_name:VARCHAR(255), phone_number:VARCHAR(50), email:VARCHAR(255), id_type:VARCHAR(50), id_number_encrypted:VARCHAR(255), gender:VARCHAR(20), contact_status:VARCHAR(50))
* **[P1] 功能:** 统一联系人档案、证件管理、联系人标签[P2]、联系人偏好[P2]、会员信息管理。
#### 4.2.5 会员营销 (储值、等级、权益) [P2]
* **功能:** 储值管理、会员等级定义与升级、会员权益定义与关联。
#### 4.2.6 租户级系统管理 [P0]
* **功能:**
* **员工管理:** 租户管理员管理内部员工账号。
* **角色与权限:** 租户管理员定义内部角色 [MVP: 预设固定角色]。
* **基础设置:** 民宿信息、Logo等房型管理房间管理基础定价策略 (`pms_room_types.default_price`, `pms_room_pricing_rules`表);支付方式管理 (`pms_finance_payment_methods`);附加消费项目 (`pms_finance_extra_charge_items`)。
* **门店管理:** 在一个租户下管理多个门店每个门店对应一个sys_dept记录。
* **租户特定配置:** 管理租户全局及各门店特定的配置。
* **关键数据:**
* `pms_tenant_settings` (setting_id:BIGINT:pk, tenant_id:VARCHAR(20), dept_id:BIGINT, setting_group:VARCHAR(100), setting_key:VARCHAR(100), setting_value:TEXT, value_type:VARCHAR(50), description:TEXT, is_sensitive:BOOLEAN)
* 特殊说明: 保留tenant_id字段当dept_id为NULL时表示租户全局配置当dept_id有值时表示特定门店配置。主要存储PMS业务相关的配置项。
* `pms_mp_settings` (setting_id:BIGINT:pk, tenant_id:VARCHAR(20), dept_id:BIGINT, setting_key:VARCHAR(100), setting_value:TEXT, value_type:VARCHAR(50), description:TEXT)
* 特殊说明: 保留tenant_id字段当dept_id为NULL时表示租户级小程序配置当dept_id有值时表示特定门店的小程序配置。主要存储民宿管理小程序的界面和功能配置。
* `pms_tenant_user_devices` (id:BIGINT:pk, user_id:BIGINT, device_type:VARCHAR(50), device_token:VARCHAR(512), app_version:VARCHAR(50), last_login_at:DATETIME, status:VARCHAR(50))
* `pms_room_pricing_rules` (rule_id:BIGINT:pk, dept_id:BIGINT, name:VARCHAR(255), room_type_id:BIGINT, date_range_start:DATE, date_range_end:DATE, days_of_week_json:JSON, min_length_of_stay:INT, max_length_of_stay:INT, price_adjustment_type:VARCHAR(50), adjustment_value:DECIMAL(10,2), priority:INT, status:VARCHAR(50))
##### 4.2.6.1 员工管理
* **功能:** 民宿租户管理员管理本民宿的员工账号、分配角色。用户登录后,若关联多个租户或部门,提供切换租户和部门(门店)功能。
#### 4.2.7 简易报表与分析 [P1]
* **功能:** 核心运营报表 (总营收、入住率、ADR、RevPAR)、渠道来源分析、会员统计、消费分析。
* **特点:** 支持门店级和租户级统计分析。
#### 4.2.8 民宿管理小程序 (Owner We App) [P0]
* **技术:** 前端使用 Uni-App
* **功能:** 房态查看、订单提醒、日程查看(今日预抵/预离)、状态处理 (简单入住/退房操作)、数据概览(核心收支数据)。
* **特点:** 支持门店切换功能。
#### 4.2.9 PMS核心管理后台 [P0]
* **描述:** 提供独立的Web管理后台界面完成所有PMS核心业务操作。
## 5. 关键业务场景模型图 (PMS内部)
### 5.1 订单状态图 - 房间预订生命周期
```mermaid
stateDiagram-v2
[*] --> 待确认: 用户创建预订
待确认 --> 已确认: 预付款成功/人工确认
待确认 --> 已取消: 超时未确认/用户取消
已确认 --> 已取消: 申请取消并退款
已确认 --> 已入住: 办理入住手续
已入住 --> 已退房: 办理退房手续
已退房 --> 已完成: 完成结算
已取消 --> [*]
已完成 --> [*]
```
### 5.2 PMS核心业务状态图
```mermaid
stateDiagram-v2
state 房间状态 {
[*] --> 空闲
空闲 --> 已预订: 创建预订
空闲 --> 维护中: 设置维护
已预订 --> 已入住: 办理入住
已预订 --> 空闲: 取消预订
已入住 --> 待清洁: 办理退房
待清洁 --> 清洁中: 开始清洁
清洁中 --> 空闲: 完成清洁
维护中 --> 空闲: 完成维护
}
state 财务状态 {
[*] --> 未结账
未结账 --> 部分支付: 收取定金/押金
部分支付 --> 部分支付: 增加消费
部分支付 --> 已结清: 支付尾款
部分支付 --> 退款中: 申请退款
退款中 --> 已退款: 确认退款
已结清 --> [*]
已退款 --> [*]
}
```
### 5.3 PMS租户核心业务子系统组件图
```mermaid
flowchart TD
subgraph PMS["PMS租户核心业务子系统"]
direction TB
APILayer["API层 (内部)"]:::internal --> |请求分发| BusinessModules
subgraph BusinessModules["核心业务模块"]
direction TB
Room["房态管理"]:::internal
Booking["订单管理"]:::internal
Finance["财务与支付"]:::internal
Guest["联系人与会员 (P1核心)"]:::internal
Settings["系统管理 (含门店管理)"]:::internal
Reports["报表分析 (P1+)"]:::internal
Room <--> Booking
Booking <--> Finance
Booking --> Guest
end
end
subgraph AccessLayer["应用访问层 (PMS用户)"]
direction LR
MiniApp["民宿管理小程序"]:::external
WebAdmin["PMS Web管理后台"]:::external
end
AccessLayer --> APILayer
classDef internal fill:#f0f8ff,stroke:#1890ff,stroke-width:1px
classDef external fill:#f5f5f5,stroke:#d9d9d9,stroke-width:1px
```
## 6. 系统角色与权限概述 (PMS相关用户)
1. **民宿租户管理员 (Tenant Admin):**
* 用户实体: 关联到 `sys_user` 表。
* 关联: 通过 `sys_user_role` 表关联到租户及相关角色。
* 操作平台: PMS核心后台 (Web), 民宿管理小程序 (Uni-App)。
* 核心职责: 管理其名下一个或多个民宿门店的完整运营,包括房型房间、订单、财务、员工、租户级系统设置等。
2. **门店管理员 (Store Manager):**
* 用户实体: 关联到 `sys_user` 表。
* 关联: 通过 `sys_user_role` 表和 `sys_dept` 表关联到特定门店与角色。
* 操作平台: PMS核心后台 (Web), 民宿管理小程序 (Uni-App)。
* 核心职责: 管理特定门店的日常运营。
3. **民宿前台/员工 (Tenant Staff):**
* 用户实体: 关联到 `sys_user` 表。
* 关联: 通过 `sys_user_role` 表和 `sys_dept` 表关联到门店及相关角色。
* 操作平台: PMS核心后台 (Web), 民宿管理小程序 (Uni-App)。
* 核心职责: 根据分配的权限执行日常操作,如订单处理、房态更新、联系人入住退房、账务录入等。
## 7. 实施策略与演进路径 (PMS P0重点)
### 7.1 MVP阶段 (PMS核心功能)
- **房态管理:** 基础房型、房间定义和日历视图。
- **订单管理:** 手动录单、接收外部订单(简化接口)、入住、退房基本流程。
- **财务与支付:** 基础账单 (Folio)、交易流水、手动收款/支付记录。
- **租户级系统管理:** 员工账号、门店管理、基础设置(房型、房间、基础价格、支付方式、附加消费项目)。
- **民宿管理小程序:** 房态查看和简单操作(入住/退房)。
## 8. 版本历史
- v2.8 (原始文档版本) - 本文档基于此裁剪和聚焦。
- v3.0 (最新版本) - 修改为"部门作为门店"的数据模型业务表只关联sys_dept。
## 附录 (PMS内部核心实体)
### 附录A 概念性 E-R 图 (PMS核心)
```mermaid
graph LR
sys_tenant(sys_tenant) -->|1..N| sys_dept(Department)
sys_dept -->|1..N| pms_core_orders(Order)
sys_dept -->|1..N| pms_room_rooms(Room)
sys_dept -->|1..N| sys_user(User)
sys_dept -->|1..N| pms_tenant_settings(Settings)
pms_core_orders -->|0..1| cmn_contacts(Contact)
pms_core_orders -->|1..N| pms_core_order_items(OrderItem)
pms_core_orders -->|1..1| pms_finance_folios(Folio)
pms_room_rooms -->|1..1| pms_room_types(RoomType)
pms_finance_folios -->|1..N| pms_finance_transactions(Transaction)
```
### 附录B 实体关系图 (PMS核心及关联)
```mermaid
graph LR
sys_tenant("sys_tenant") -->|1..N| sys_dept("sys_dept (门店)")
sys_dept -->|1..N| pms_core_orders
sys_dept -->|1..N| pms_room_rooms
sys_dept -->|1..N| sys_user
sys_user -->|1..N| pms_tenant_user_devices
sys_user -->|1..N| sys_user_role
sys_dept -->|1..N| pms_tenant_settings
sys_dept -->|1..N| pms_mp_settings
sys_dept -->|1..N| cmn_contacts
sys_dept -->|1..N| pms_room_types
pms_core_orders -->|0..1| cmn_contacts
pms_core_orders -->|1..N| pms_core_order_items
pms_core_orders -->|1..1| pms_finance_folios
pms_room_rooms -->|1..1| pms_room_types
pms_room_types -->|1..N| pms_room_pricing_rules
pms_core_order_items -->|0..1| pms_finance_extra_charge_items
pms_core_order_items -->|0..1| pms_room_rooms
pms_finance_folios -->|1..N| pms_finance_transactions
pms_finance_transactions -->|0..1| pms_finance_payment_methods
```

288
docs/二开todolist.md Normal file
View File

@ -0,0 +1,288 @@
# PMS 模块二次开发 ToDoList
## 阶段 0: 项目初始化与理解
- [x] **阅读核心文档:**
- [x] 仔细阅读《PMS数据模型.md》完全理解PMS核心表的结构、字段定义、关系以及主键/外键/索引规范。
- [x] 仔细阅读《PMS需求.md》明确PMS核心模块 [P0] 阶段必须实现的功能需求(房态、订单核心生命周期、基础财务支付、租户级系统管理基础)。
- [x] 通读《RuoYi-Vue-Plus二次开发最佳实践.md》重点关注以下章节
- Chapter 1: 引言 (文档目的, 模块化特性, **二次开发核心原则**, **Cursor使用优势**)
- Chapter 2: 环境准备与项目结构 (前后端环境, **前后端项目结构**, **代码生成器与Cursor协同**)
- Chapter 3: 模块深入分析 (用户管理模块的启示)
- Chapter 4: 后端开发最佳实践 (Cursor辅助技巧, 模块设计, 分层约定, Domain对象规范)
- Chapter 5: 前端开发最佳实践 (Cursor辅助技巧, 项目结构, 功能模块开发规范, 组件案例, 前后端交互)
- Chapter 6: 前后端协作规范
- Chapter 7: 代码规范与风格
- Chapter 8: 新模块添加流程与实践
- [x] **环境与工具准备:**
- [x] 确保后端开发环境 (JDK, Maven, Redis, MySQL) 符合《最佳实践》Chapter 2.1 要求。
- [x] 确保前端开发环境 (Node.js, pnpm) 符合《最佳实践》Chapter 2.1 要求。
- [x] 安装并配置好 Cursor IDE将整个 `ruoyi-vue-plus` 项目导入工作区。
- [x] (可选)在 Cursor 中为项目设置特定上下文,如关键模块路径、技术栈等,以便更好地辅助开发。
## 阶段 1: 后端开发 - 模块搭建与核心表结构
- [ ] **创建后端Maven子模块 `ruoyi-pms`** (参考《最佳实践》Chapter 2.2, 4.2, 8.3):
- [ ] 在 `ruoyi-vue-plus/ruoyi-modules` 目录下创建新的 Maven 子模块 `ruoyi-pms`
- [ ] 配置 `ruoyi-pms/pom.xml`
- [ ] 设置 `<parent>` 指向 `ruoyi-modules`
- [ ] 添加必要的公共模块依赖 (如: `ruoyi-common-core`, `ruoyi-common-mybatis`, `ruoyi-common-web`, `ruoyi-common-satoken`, `ruoyi-common-excel`, `ruoyi-common-translate`, `ruoyi-common-tenant`)。
- [ ] 在项目根目录 `pom.xml``ruoyi-modules/pom.xml``<modules>` 部分注册 `ruoyi-pms`
- [ ] **创建后端基础包结构** (参考《最佳实践》Chapter 4.2):
- [ ] 在 `ruoyi-pms/src/main/java/` 下创建基础包,例如 `org.dromara.pms`
- [ ] 在 `org.dromara.pms` 下创建标准分层包:`controller`, `service`, `service.impl`, `mapper`, `domain` (包含 `entity`, `bo`, `vo`)。
- [ ] **数据库表创建与初始化**:
- [ ] **执行SQL脚本**: 确保已在开发数据库中执行 `script/sql/pms_tables.sql` 文件创建所有PMS相关的表。
- [ ] **验证表结构**: 对照《PMS数据模型.md》仔细检查已创建的表结构、字段类型、约束、索引是否正确。
- [ ] **验证基础数据**: 确认 `pms_core_channels``pms_finance_payment_methods` 表的基础数据已按 `pms_tables.sql` 中的`INSERT`语句正确插入。
- [ ] **集成新模块到系统**:
- [ ] (如果需要)在 `ruoyi-admin` 模块的 `application.yml` 中,确保新模块的包路径 (如 `org.dromara.pms`) 被扫描到。
- [ ] (如果需要)配置MyBatis Plus扫描新模块的Mapper XML文件路径。
- [ ] **后端国际化资源文件**:
- [ ] 在 `ruoyi-pms/src/main/resources/` 下创建 `i18n` 目录。
- [ ] 在 `i18n` 目录下创建基础的国际化属性文件,如 `messages_zh_CN.properties``messages_en_US.properties`
## 阶段 2: 后端开发 - 核心业务功能 [P0]
**通用后端开发规范 (参考《最佳实践》Chapter 4 & 7.1):**
- [ ] 遵循分层架构约定 (Controller -> Service -> Mapper)。
- [ ] 严格区分 Entity, BO, VO 的职责和使用场景。
- [ ] 使用 MapStruct Plus (`@AutoMapper`) 进行对象转换。
- [ ] Service 层实现类的方法应添加 `@Transactional` 注解。
- [ ] Controller 层方法应添加 `@Log` 操作日志注解和 `@SaCheckPermission` 权限注解。
- [ ] BO 对象字段使用 JSR 303/380 注解进行参数校验。
- [ ] VO 对象字段按需使用 `@Translation``@Sensitive` 注解。
- [ ] 编写清晰的 Javadoc 注释和行内注释。
- [ ] 使用 Cursor 辅助生成代码、分析逻辑、优化代码。
**具体功能模块开发:**
- [ ] **代码生成器应用 (可选但推荐)** (参考《最佳实践》Chapter 2.4):
- [ ] 针对《PMS数据模型.md》中定义的核心表 (如 `pms_room_types`, `pms_room_rooms`, `pms_core_orders` 等),使用若依代码生成器生成初始的 Entity, Mapper, Service, Controller, BO, VO。
- [ ] **Cursor辅助**: "请Cursor分析代码生成器为 `pms_room_types` 表生成的后端代码并指出哪些部分需要根据《PMS需求.md》和《最佳实践》进行调整。"
- [ ] **1. 房型管理 (`pms_room_types`)**:
- [ ] **Domain**: 创建/调整 `PmsRoomType`, `PmsRoomTypeBo`, `PmsRoomTypeVo`
- [ ] **Mapper**: 创建/调整 `PmsRoomTypeMapper.java` 和对应的 `PmsRoomTypeMapper.xml`
- [ ] **Service**: 创建/调整 `IPmsRoomTypeService.java``PmsRoomTypeServiceImpl.java` (实现CRUD逻辑)。
- [ ] **Controller**: 创建/调整 `PmsRoomTypeController.java` (暴露RESTful API)。
- [ ] **2. 房间管理 (`pms_room_rooms`)**:
- [ ] **Domain**: `PmsRoomRoom`, `PmsRoomRoomBo`, `PmsRoomRoomVo`.
- [ ] **Mapper**: `PmsRoomRoomMapper.java` & XML.
- [ ] **Service**: `IPmsRoomRoomService.java` & Impl (CRUD, 更新房间状态/清洁状态逻辑).
- [ ] **Controller**: `PmsRoomRoomController.java`.
- [ ] **3. 房间锁定 (`pms_room_locks`)**:
- [ ] **Domain**: `PmsRoomLock`, `PmsRoomLockBo`, `PmsRoomLockVo`.
- [ ] **Mapper**: `PmsRoomLockMapper.java` & XML.
- [ ] **Service**: `IPmsRoomLockService.java` & Impl (CRUD).
- [ ] **Controller**: `PmsRoomLockController.java`.
- [ ] **4. 核心订单管理 (`pms_core_orders`, `pms_core_order_items`)** (参考《PMS需求.md》4.2.2 [P0]):
- [ ] **Domain**:
- `PmsCoreOrder`, `PmsCoreOrderBo`, `PmsCoreOrderVo` (应包含订单项列表).
- `PmsCoreOrderItem`, `PmsCoreOrderItemBo`, `PmsCoreOrderItemVo`.
- [ ] **Mapper**: `PmsCoreOrderMapper`, `PmsCoreOrderItemMapper` & XMLs.
- [ ] **Service**: `IPmsCoreOrderService`, `IPmsCoreOrderItemService` & Impls (实现订单创建、查询、修改、取消、状态流转核心逻辑;订单项增删改查).
- [ ] 实现订单状态转换逻辑 (pending_confirmation -> confirmed -> checked_in -> checked_out / cancelled / no_show).
- [ ] 订单创建时关联/创建 `cmn_contacts` (基础字段)。
- [ ] 订单创建时关联 `pms_core_channels`
- [ ] 订单创建/确认时,自动创建或关联 `pms_finance_folios`
- [ ] **Controller**: `PmsCoreOrderController`.
- [ ] **5. 订单来源渠道 (`pms_core_channels`)**:
- [ ] **Domain**: `PmsCoreChannel`, `PmsCoreChannelBo`, `PmsCoreChannelVo`.
- [ ] **Mapper**: `PmsCoreChannelMapper` & XML.
- [ ] **Service**: `IPmsCoreChannelService` & Impl (CRUD).
- [ ] **Controller**: `PmsCoreChannelController`.
- [ ] **6. 财务-账单管理 (`pms_finance_folios`)** (参考《PMS需求.md》4.2.3 [P0]):
- [ ] **Domain**: `PmsFinanceFolio`, `PmsFinanceFolioBo`, `PmsFinanceFolioVo` (应包含交易流水列表).
- [ ] **Mapper**: `PmsFinanceFolioMapper` & XML.
- [ ] **Service**: `IPmsFinanceFolioService` & Impl (CRUD, 更新账单状态, 计算余额逻辑 - 应用层计算).
- [ ] **Controller**: `PmsFinanceFolioController`.
- [ ] **7. 财务-交易流水 (`pms_finance_transactions`)**:
- [ ] **Domain**: `PmsFinanceTransaction`, `PmsFinanceTransactionBo`, `PmsFinanceTransactionVo`.
- [ ] **Mapper**: `PmsFinanceTransactionMapper` & XML.
- [ ] **Service**: `IPmsFinanceTransactionService` & Impl (CRUD, 关联 `pms_finance_payment_methods`).
- [ ] **Controller**: `PmsFinanceTransactionController`.
- [ ] **8. 财务-支付方式 (`pms_finance_payment_methods`)**:
- [ ] **Domain**: `PmsFinancePaymentMethod`, `PmsFinancePaymentMethodBo`, `PmsFinancePaymentMethodVo`.
- [ ] **Mapper**: `PmsFinancePaymentMethodMapper` & XML.
- [ ] **Service**: `IPmsFinancePaymentMethodService` & Impl (CRUD).
- [ ] **Controller**: `PmsFinancePaymentMethodController`.
- [ ] **9. 财务-附加费用项目 (`pms_finance_extra_charge_items`)**:
- [ ] **Domain**: `PmsFinanceExtraChargeItem`, `PmsFinanceExtraChargeItemBo`, `PmsFinanceExtraChargeItemVo`.
- [ ] **Mapper**: `PmsFinanceExtraChargeItemMapper` & XML.
- [ ] **Service**: `IPmsFinanceExtraChargeItemService` & Impl (CRUD).
- [ ] **Controller**: `PmsFinanceExtraChargeItemController`.
- [ ] **10. 价格规则 (`pms_room_pricing_rules`)**:
- [ ] **Domain**: `PmsRoomPricingRule`, `PmsRoomPricingRuleBo`, `PmsRoomPricingRuleVo`.
- [ ] **Mapper**: `PmsRoomPricingRuleMapper` & XML.
- [ ] **Service**: `IPmsRoomPricingRuleService` & Impl (CRUD).
- [ ] **Controller**: `PmsRoomPricingRuleController`.
- [ ] **11. 联系人基础 (`cmn_contacts`)** (P0阶段主要为订单关联所需字段):
- [ ] **Domain**: `CmnContact`, `CmnContactBo`, `CmnContactVo` (仅包含P0阶段所需字段如姓名、电话).
- [ ] **Mapper**: `CmnContactMapper` & XML.
- [ ] **Service**: `ICmnContactService` & Impl (提供基础的联系人查询、创建接口供订单模块调用).
- [ ] **Controller**: (P0阶段可能不需要完整独立的Controller主要通过订单业务间接操作).
- [ ] **12. 租户/部门特定配置 (`pms_tenant_settings`, `pms_mp_settings`)**:
- [ ] **Domain**: `PmsTenantSetting`, `PmsTenantSettingBo`, `PmsTenantSettingVo` & 类似的 `PmsMpSetting` 对象.
- [ ] **Mapper**: `PmsTenantSettingMapper`, `PmsMpSettingMapper` & XMLs.
- [ ] **Service**: `IPmsTenantSettingService`, `IPmsMpSettingService` & Impls (CRUD, 按 group/key 查询).
- [ ] **Controller**: `PmsTenantSettingController`, `PmsMpSettingController`.
- [ ] **13. 租户用户设备 (`pms_tenant_user_devices`)**: (用于民宿管理小程序推送等)
- [ ] **Domain**: `PmsTenantUserDevice`, `PmsTenantUserDeviceBo`, `PmsTenantUserDeviceVo`.
- [ ] **Mapper**: `PmsTenantUserDeviceMapper` & XML.
- [ ] **Service**: `IPmsTenantUserDeviceService` & Impl (CRUD, 设备注册/更新逻辑).
- [ ] **Controller**: `PmsTenantUserDeviceController`.
- [ ] **API文档**:
- [ ] 为所有Controller和DTO添加清晰的Swagger/Knife4j注解 (参考《最佳实践》Chapter 8.3)。
## 阶段 3: 前端开发 - 核心管理界面 [P0] (Soybean Admin Pro)
**通用前端开发规范 (参考《最佳实践》Chapter 5 & 7.2):**
- [ ] 遵循 Soybean Admin Pro 的项目结构和编码规范。
- [ ] 使用 Vue 3 Composition API + `<script setup>`
- [ ] 为 props, emits, reactive state, methods 提供 TypeScript 类型。
- [ ] 优先使用框架提供的 Hooks (`useTable`, `useForm` 等) 和 Naive UI 组件。
- [ ] 组件样式使用 scoped CSS 或 CSS Modules。
- [ ] 使用 Cursor 辅助生成组件、分析代码、实现逻辑。
**具体功能模块开发:**
- [ ] **前端项目结构搭建** (参考《最佳实践》Chapter 5.2):
- [ ] 在 `src/views/`下创建 `pms` 目录,并为各管理功能创建子目录 (如 `room-type`, `room`, `order`, `folio` 等)。
- [ ] 在 `src/service/api/` 下创建 `pms` 目录并为各后端Controller创建对应的 `ts` 服务文件 (如 `roomType.ts`, `order.ts`)。
- [ ] 在 `src/typings/api/` 下创建 `pms` 目录定义与后端BO/VO对应的TypeScript接口。
- [ ] 创建 `src/router/routes/modules/pms.ts` 路由配置文件。
- [ ] (可选) 在 `src/store/modules/` 下创建 `pms` 目录用于存放PMS相关的Pinia store。
- [ ] **Cursor辅助**: "请Cursor根据《最佳实践》前端项目结构为PMS模块生成基础的目录和文件占位符。"
- [ ] **1. 房型管理界面 (`src/views/pms/room-type/`)**:
- [ ] **API服务**: `src/service/api/pms/roomType.ts` (实现对后端 `PmsRoomTypeController` 的调用)。
- [ ] **类型定义**: `src/typings/api/pms/roomType.ts` (定义 `PmsRoomTypeVo` 等接口)。
- [ ] **列表页**: `index.vue` (表格展示房型列表,支持搜索、分页、新增、编辑、删除按钮)。
- [ ] **表单组件**: `components/RoomTypeForm.vue` (用于新增/编辑房型的弹窗或抽屉表单,包含字段校验)。
- [ ] **2. 房间管理界面 (`src/views/pms/room/`)**:
- [ ] **API服务**: `src/service/api/pms/room.ts`.
- [ ] **类型定义**: `src/typings/api/pms/room.ts`.
- [ ] **列表页**: `index.vue` (展示房间列表,支持按房型、状态等搜索,操作按钮)。
- [ ] **表单组件**: `components/RoomForm.vue`.
- [ ] **房态图/日历视图基础**: (《PMS需求.md》4.2.1 [P0] 网格日历视图 - 此为复杂功能P0阶段可能先实现基础列表和状态管理)
- [ ] **3. 订单管理界面 (`src/views/pms/order/`)**:
- [ ] **API服务**: `src/service/api/pms/order.ts`.
- [ ] **类型定义**: `src/typings/api/pms/order.ts`.
- [ ] **列表页**: `index.vue` (展示订单列表,支持按日期、状态、联系人等搜索)。
- [ ] **详情页/表单**: `detail.vue``components/OrderForm.vue` (用于创建/编辑/查看订单详情,包含订单项管理)。
- [ ] 实现订单状态变更操作。
- [ ] 关联联系人选择/创建。
- [ ] 关联房型/房间选择。
- [ ] **4. 财务-账单管理界面 (`src/views/pms/folio/`)**:
- [ ] **API服务**: `src/service/api/pms/folio.ts`.
- [ ] **类型定义**: `src/typings/api/pms/folio.ts`.
- [ ] **列表页/详情页**: (展示账单列表,点击可查看账单详情及交易流水)。
- [ ] **5. 财务-支付方式管理界面 (`src/views/pms/payment-method/`)**:
- [ ] **API服务**: `src/service/api/pms/paymentMethod.ts`.
- [ ] **类型定义**: `src/typings/api/pms/paymentMethod.ts`.
- [ ] **列表页**: `index.vue` (管理支付方式)。
- [ ] **表单组件**: `components/PaymentMethodForm.vue`.
- [ ] **6. 财务-附加费用项目管理界面 (`src/views/pms/extra-charge-item/`)**:
- [ ] **API服务**: `src/service/api/pms/extraChargeItem.ts`.
- [ ] **类型定义**: `src/typings/api/pms/extraChargeItem.ts`.
- [ ] **列表页**: `index.vue`.
- [ ] **表单组件**: `components/ExtraChargeItemForm.vue`.
- [ ] **7. 价格规则管理界面 (`src/views/pms/pricing-rule/`)**:
- [ ] **API服务**: `src/service/api/pms/pricingRule.ts`.
- [ ] **类型定义**: `src/typings/api/pms/pricingRule.ts`.
- [ ] **列表页**: `index.vue`.
- [ ] **表单组件**: `components/PricingRuleForm.vue`.
- [ ] **8. 租户/部门特定配置界面 (`src/views/pms/settings/`)**:
- [ ] (按需实现,可能集成到系统现有配置管理或单独页面)
- [ ] **路由配置 (`src/router/routes/modules/pms.ts`)**:
- [ ] 为以上所有PMS管理页面配置路由。
- [ ] 配置菜单项 (名称、图标、排序、权限标识) (参考《最佳实践》Chapter 5.3)。
- [ ] **Cursor辅助**: "请Cursor根据已创建的PMS前端页面生成对应的 `pms.ts` 路由配置文件,并包含菜单定义。"
- [ ] **前端国际化**:
- [ ] 将所有界面上的文本添加到 `src/locales/langs/zh-CN.ts``en-US.ts` 中。
- [ ] **权限控制**:
- [ ] 使用 `useAuth` Hook 和权限指令,根据后端返回的权限标识控制按钮的显示/隐藏和页面的访问。
## 阶段 4: 民宿管理小程序 (Owner We App) [P0] - 基础功能
**技术栈**: Uni-App (参考《PMS需求.md》4.2.8)
- [ ] **项目搭建**: 创建 Uni-App 项目。
- [ ] **API对接**:
- [ ] 封装调用后端PMS接口的请求函数 (复用或参考前端Web的API服务)。
- [ ] 处理用户认证和token。
- [ ] **核心功能实现**:
- [ ] **房态查看**:
- [ ] 调用后端接口获取房态数据。
- [ ] 以列表或简化日历形式展示房间状态。
- [ ] **订单提醒**: (P0阶段可能为简单的订单列表按预抵/预离排序)
- [ ] 调用后端接口获取订单列表。
- [ ] **日程查看 (今日预抵/预离)**:
- [ ] 筛选并展示当日预抵和预离的订单。
- [ ] **状态处理 (简单入住/退房操作)**:
- [ ] 提供按钮或操作,调用后端接口更新订单状态为 `checked_in``checked_out`
- [ ] **数据概览 (核心收支数据)**: (P0阶段可简化如当日收款总额)
- [ ] **门店切换功能**: 如果用户关联多个门店,允许切换当前操作的门店上下文。
- [ ] **UI/UX设计**: 简洁易用,符合移动端操作习惯。
- [ ] **打包与测试**: 在微信开发者工具和真机上进行测试。
## 阶段 5: 集成、测试与部署
- [ ] **后端单元测试**:
- [ ] 为核心Service层方法编写JUnit单元测试。
- [ ] **前后端联调** (参考《最佳实践》Chapter 6, 8.5):
- [ ] 逐个功能模块进行前后端接口联调,确保数据交互正确。
- [ ] **Cursor辅助**: "后端 `PmsOrderController``getOrderById` 接口返回数据格式与前端期望不一致请Cursor分析可能的原因。"
- [ ] **系统集成测试**:
- [ ] 测试PMS模块与若依基础模块 (用户、部门、租户、权限、字典等) 的集成情况。
- [ ] 测试多租户、多部门数据隔离是否正确。
- [ ] **[P0] 功能验收测试 (UAT)**:
- [ ] 根据《PMS需求.md》中[P0]功能点,逐一进行业务场景测试。
- [ ] **Bug修复与回归测试**:
- [ ] **数据库菜单与权限SQL**:
- [ ] 编写将PMS模块前端路由配置的菜单信息插入到 `sys_menu` 表的SQL脚本。
- [ ] (如果需要新的权限标识) 编写插入到 `sys_permission` (或其他权限相关表) 的SQL脚本并关联到角色。
- [ ] **文档完善**:
- [ ] 更新/创建PMS模块相关的开发文档、API文档 (确保Swagger/Knife4j是最新的)。
- [ ] **部署准备** (参考《最佳实践》Chapter 8.5):
- [ ] 后端打包 (如 `mvn package -DskipTests`)。
- [ ] 前端打包 (`pnpm build`)。
- [ ] 小程序打包。
- [ ] **部署到测试/生产环境并验证**
## 阶段 6: 持续迭代与优化 [P1+]
- [ ] 根据《PMS需求.md》中的 [P1], [P2] 优先级,规划后续迭代功能。
- [ ] **订单管理[P1]**: 订单修改、款项管理、取消退款细则、自动化通知等。
- [ ] **联系人与会员管理[P1]**: 统一联系人档案、证件管理。
- [ ] **简易报表与分析[P1]**: 核心运营报表。
- [ ] 性能优化。
- [ ] 用户体验改进。
- [ ] 安全加固。
---
**注意**: 此ToDoList仅为初步计划具体执行时需根据实际情况灵活调整。请频繁使用Cursor进行代码生成、分析、重构和问题排查以提高开发效率和代码质量。

514
pom.xml Normal file
View File

@ -0,0 +1,514 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-vue-plus</artifactId>
<version>${revision}</version>
<name>RuoYi-Vue-Plus</name>
<url>https://gitee.com/dromara/RuoYi-Vue-Plus</url>
<description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
<properties>
<revision>5.3.1</revision>
<spring-boot.version>3.4.4</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<mybatis.version>3.5.16</mybatis.version>
<springdoc.version>2.8.5</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<easyexcel.version>4.0.3</easyexcel.version>
<velocity.version>2.3</velocity.version>
<satoken.version>1.40.0</satoken.version>
<mybatis-plus.version>3.5.11</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version>
<hutool.version>5.8.35</hutool.version>
<spring-boot-admin.version>3.4.5</spring-boot-admin.version>
<redisson.version>3.45.1</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version>
<snailjob.version>1.4.0</snailjob.version>
<mapstruct-plus.version>1.4.6</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.36</lombok.version>
<bouncycastle.version>1.76</bouncycastle.version>
<justauth.version>1.16.7</justauth.version>
<!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version>
<!-- OSS 配置 -->
<aws.sdk.version>2.28.22</aws.sdk.version>
<!-- SMS 配置 -->
<sms4j.version>3.3.4</sms4j.version>
<!-- 限制框架中的fastjson版本 -->
<fastjson.version>1.2.83</fastjson.version>
<!-- 面向运行时的D-ORM依赖 -->
<anyline.version>8.7.2-20250101</anyline.version>
<!--工作流配置-->
<warm-flow.version>1.6.8</warm-flow.version>
<!-- 插件版本 -->
<maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
<maven-war-plugin.version>3.2.2</maven-war-plugin.version>
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
<!-- 打包默认跳过测试 -->
<skipTests>true</skipTests>
</properties>
<profiles>
<profile>
<id>local</id>
<properties>
<!-- 环境标识,需要与配置文件的名称相对应 -->
<profiles.active>local</profiles.active>
<logging.level>info</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
</properties>
</profile>
<profile>
<id>dev</id>
<properties>
<!-- 环境标识,需要与配置文件的名称相对应 -->
<profiles.active>dev</profiles.active>
<logging.level>info</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
</properties>
<activation>
<!-- 默认环境 -->
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>prod</id>
<properties>
<profiles.active>prod</profiles.active>
<logging.level>warn</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
</properties>
</profile>
</profiles>
<!-- 依赖声明 -->
<dependencyManagement>
<dependencies>
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- hutool 的依赖配置-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-bom</artifactId>
<version>${hutool.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Warm-Flow国产工作流引擎, 在线文档http://warm-flow.cn/ -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<!-- JustAuth 的依赖配置-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
<!-- common 的依赖配置-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-bom</artifactId>
<version>${revision}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc</artifactId>
<version>${therapi-javadoc.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
<!-- velocity代码生成使用模板 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<!-- Sa-Token 权限认证, 在线文档http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${satoken.version}</version>
</dependency>
<!-- Sa-Token 整合 jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>${satoken.version}</version>
<exclusions>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
<version>${satoken.version}</version>
</dependency>
<!-- dynamic-datasource 多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${dynamic-ds.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-annotation</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- sql性能分析插件 -->
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>${p6spy.version}</version>
</dependency>
<!-- AWS SDK for Java 2.x -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!--短信sms4j-->
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-spring-boot-starter</artifactId>
<version>${sms4j.version}</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
<version>${lock4j.version}</version>
</dependency>
<!-- SnailJob Client -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-starter</artifactId>
<version>${snailjob.version}</version>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-job-core</artifactId>
<version>${snailjob.version}</version>
</dependency>
<!-- 加密包引入 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
<version>${mapstruct-plus.version}</version>
</dependency>
<!-- 离线IP地址定位库 ip2region -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>${ip2region.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-system</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-job</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-generator</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
<version>${revision}</version>
</dependency>
<!-- 工作流模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-workflow</artifactId>
<version>${revision}</version>
</dependency>
<!-- PMS民宿管理系统模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-pms</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>ruoyi-admin</module>
<module>ruoyi-common</module>
<module>ruoyi-extend</module>
<module>ruoyi-modules</module>
</modules>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc-scribe</artifactId>
<version>${therapi-javadoc.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
</path>
<path>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-processor</artifactId>
<version>${mapstruct-plus.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${mapstruct-plus.lombok.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<!-- 单元测试使用 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<argLine>-Dfile.encoding=UTF-8</argLine>
<!-- 根据打包环境执行对应的@Tag测试方法 -->
<groups>${profiles.active}</groups>
<!-- 排除标签 -->
<excludedGroups>exclude</excludedGroups>
</configuration>
</plugin>
<!-- 统一版本号管理 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>${flatten-maven-plugin.version}</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<!-- 关闭过滤 -->
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<!-- 引入所有 匹配文件进行过滤 -->
<includes>
<include>application*</include>
<include>bootstrap*</include>
<include>banner*</include>
</includes>
<!-- 启用过滤 即该资源中的变量将会被过滤器中的值替换 -->
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>public</id>
<name>huawei nexus</name>
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>huawei nexus</name>
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>

30
ruoyi-admin/Dockerfile Normal file
View File

@ -0,0 +1,30 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
#FROM bellsoft/liberica-openjdk-debian:21.0.5-cds
#FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li"
RUN mkdir -p /ruoyi/server/logs \
/ruoyi/server/temp \
/ruoyi/skywalking/agent
WORKDIR /ruoyi/server
ENV SERVER_PORT=8080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
EXPOSE ${SERVER_PORT}
ADD ./target/ruoyi-admin.jar ./app.jar
# 工作流字体文件
ADD ./zhFonts/ /usr/share/fonts/zhFonts/
SHELL ["/bin/bash", "-c"]
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
# 应用名称 如果想区分集群节点监控 改成不同的名称即可
#-Dskywalking.agent.service_name=ruoyi-server \
#-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
-jar app.jar

159
ruoyi-admin/pom.xml Normal file
View File

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi-vue-plus</artifactId>
<groupId>org.dromara</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>ruoyi-admin</artifactId>
<description>
web服务入口
</description>
<dependencies>
<!-- Mysql驱动包 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- &lt;!&ndash; mp支持的数据库均支持 只需要增加对应的jdbc依赖即可 &ndash;&gt;-->
<!-- &lt;!&ndash; Oracle &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>com.oracle.database.jdbc</groupId>-->
<!-- <artifactId>ojdbc8</artifactId>-->
<!-- </dependency>-->
<!-- &lt;!&ndash; 兼容oracle低版本 &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>com.oracle.database.nls</groupId>-->
<!-- <artifactId>orai18n</artifactId>-->
<!-- </dependency>-->
<!-- &lt;!&ndash; PostgreSql &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>org.postgresql</groupId>-->
<!-- <artifactId>postgresql</artifactId>-->
<!-- </dependency>-->
<!-- &lt;!&ndash; SqlServer &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>com.microsoft.sqlserver</groupId>-->
<!-- <artifactId>mssql-jdbc</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-social</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ratelimiter</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-job</artifactId>
</dependency>
<!-- 代码生成-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-demo</artifactId>
</dependency>
<!-- 工作流模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-workflow</artifactId>
</dependency>
<!-- PMS民宿管理系统模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-pms</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- skywalking 整合 logback -->
<!-- <dependency>-->
<!-- <groupId>org.apache.skywalking</groupId>-->
<!-- <artifactId>apm-toolkit-logback-1.x</artifactId>-->
<!-- <version>${与你的agent探针版本保持一致}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.apache.skywalking</groupId>-->
<!-- <artifactId>apm-toolkit-trace</artifactId>-->
<!-- <version>${与你的agent探针版本保持一致}</version>-->
<!-- </dependency>-->
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven-jar-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>${maven-war-plugin.version}</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<warName>${project.artifactId}</warName>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,23 @@
package org.dromara;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
/**
* 启动程序
*
* @author Lion Li
*/
@SpringBootApplication
public class DromaraApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(DromaraApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
System.out.println("(♥◠‿◠)ノ゙ RuoYi-Vue-Plus启动成功 ლ(´ڡ`ლ)゙");
}
}

View File

@ -0,0 +1,18 @@
package org.dromara;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
* web容器中进行部署
*
* @author Lion Li
*/
public class DromaraServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DromaraApplication.class);
}
}

View File

@ -0,0 +1,239 @@
package org.dromara.web.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.model.LoginBody;
import org.dromara.common.core.domain.model.RegisterBody;
import org.dromara.common.core.domain.model.SocialLoginBody;
import org.dromara.common.core.utils.*;
import org.dromara.common.encrypt.annotation.ApiEncrypt;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.utils.SocialUtils;
import org.dromara.common.sse.dto.SseMessageDto;
import org.dromara.common.sse.utils.SseMessageUtils;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.bo.SysTenantBo;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysTenantVo;
import org.dromara.system.service.ISysClientService;
import org.dromara.system.service.ISysConfigService;
import org.dromara.system.service.ISysSocialService;
import org.dromara.system.service.ISysTenantService;
import org.dromara.web.domain.vo.LoginTenantVo;
import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.domain.vo.TenantListVo;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.dromara.web.service.SysRegisterService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 认证
*
* @author Lion Li
*/
@Slf4j
@SaIgnore
@RequiredArgsConstructor
@RestController
@RequestMapping("/auth")
public class AuthController {
private final SocialProperties socialProperties;
private final SysLoginService loginService;
private final SysRegisterService registerService;
private final ISysConfigService configService;
private final ISysTenantService tenantService;
private final ISysSocialService socialUserService;
private final ISysClientService clientService;
private final ScheduledExecutorService scheduledExecutorService;
/**
* 登录方法
*
* @param body 登录信息
* @return 结果
*/
@ApiEncrypt
@PostMapping("/login")
public R<LoginVo> login(@RequestBody String body) {
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
ValidatorUtils.validate(loginBody);
// 授权类型和客户端id
String clientId = loginBody.getClientId();
String grantType = loginBody.getGrantType();
SysClientVo client = clientService.queryByClientId(clientId);
// 查询不到 client client 内不包含 grantType
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
return R.fail(MessageUtils.message("auth.grant.type.error"));
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
}
// 校验租户
loginService.checkTenant(loginBody.getTenantId());
// 登录
LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
Long userId = LoginHelper.getUserId();
scheduledExecutorService.schedule(() -> {
SseMessageDto dto = new SseMessageDto();
dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
dto.setUserIds(List.of(userId));
SseMessageUtils.publishMessage(dto);
}, 5, TimeUnit.SECONDS);
return R.ok(loginVo);
}
/**
* 获取跳转URL
*
* @param source 登录来源
* @return 结果
*/
@GetMapping("/binding/{source}")
public R<String> authBinding(@PathVariable("source") String source,
@RequestParam String tenantId, @RequestParam String domain) {
SocialLoginConfigProperties obj = socialProperties.getType().get(source);
if (ObjectUtil.isNull(obj)) {
return R.fail(source + "平台账号暂不支持");
}
AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
Map<String, String> map = new HashMap<>();
map.put("tenantId", tenantId);
map.put("domain", domain);
map.put("state", AuthStateUtils.createState());
String authorizeUrl = authRequest.authorize(Base64.encode(JsonUtils.toJsonString(map), StandardCharsets.UTF_8));
return R.ok("操作成功", authorizeUrl);
}
/**
* 前端回调绑定授权(需要token)
*
* @param loginBody 请求体
* @return 结果
*/
@PostMapping("/social/callback")
public R<Void> socialCallback(@RequestBody SocialLoginBody loginBody) {
// 校验token
StpUtil.checkLogin();
// 获取第三方登录信息
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
loginBody.getSource(), loginBody.getSocialCode(),
loginBody.getSocialState(), socialProperties);
AuthUser authUserData = response.getData();
// 判断授权响应是否成功
if (!response.ok()) {
return R.fail(response.getMsg());
}
loginService.socialRegister(authUserData);
return R.ok();
}
/**
* 取消授权(需要token)
*
* @param socialId socialId
*/
@DeleteMapping(value = "/unlock/{socialId}")
public R<Void> unlockSocial(@PathVariable Long socialId) {
// 校验token
StpUtil.checkLogin();
Boolean rows = socialUserService.deleteWithValidById(socialId);
return rows ? R.ok() : R.fail("取消授权失败");
}
/**
* 退出登录
*/
@PostMapping("/logout")
public R<Void> logout() {
loginService.logout();
return R.ok("退出成功");
}
/**
* 用户注册
*/
@ApiEncrypt
@PostMapping("/register")
public R<Void> register(@Validated @RequestBody RegisterBody user) {
if (!configService.selectRegisterEnabled(user.getTenantId())) {
return R.fail("当前系统没有开启注册功能!");
}
registerService.register(user);
return R.ok();
}
/**
* 登录页面租户下拉框
*
* @return 租户列表
*/
@GetMapping("/tenant/list")
public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception {
// 返回对象
LoginTenantVo result = new LoginTenantVo();
boolean enable = TenantHelper.isEnable();
result.setTenantEnabled(enable);
// 如果未开启租户这直接返回
if (!enable) {
return R.ok(result);
}
List<SysTenantVo> tenantList = tenantService.queryList(new SysTenantBo());
List<TenantListVo> voList = MapstructUtils.convert(tenantList, TenantListVo.class);
try {
// 如果只超管返回所有租户
if (LoginHelper.isSuperAdmin()) {
result.setVoList(voList);
return R.ok(result);
}
} catch (NotLoginException ignored) {
}
// 获取域名
String host;
String referer = request.getHeader("referer");
if (StringUtils.isNotBlank(referer)) {
// 这里从referer中取值是为了本地使用hosts添加虚拟域名方便本地环境调试
host = referer.split("//")[1].split("/")[0];
} else {
host = new URL(request.getRequestURL().toString()).getHost();
}
// 根据域名进行筛选
List<TenantListVo> list = StreamUtils.filter(voList, vo ->
StringUtils.equalsIgnoreCase(vo.getDomain(), host));
result.setVoList(CollUtil.isNotEmpty(list) ? list : voList);
return R.ok(result);
}
}

View File

@ -0,0 +1,154 @@
package org.dromara.web.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.mail.config.properties.MailProperties;
import org.dromara.common.mail.utils.MailUtils;
import org.dromara.common.ratelimiter.annotation.RateLimiter;
import org.dromara.common.ratelimiter.enums.LimitType;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.common.web.enums.CaptchaType;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.dromara.web.domain.vo.CaptchaVo;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.util.LinkedHashMap;
/**
* 验证码操作处理
*
* @author Lion Li
*/
@SaIgnore
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
public class CaptchaController {
private final CaptchaProperties captchaProperties;
private final MailProperties mailProperties;
/**
* 短信验证码
*
* @param phonenumber 用户手机号
*/
@RateLimiter(key = "#phonenumber", time = 60, count = 1)
@GetMapping("/resource/sms/code")
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = "";
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put("code", code);
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
if (!smsResponse.isSuccess()) {
log.error("验证码短信发送异常 => {}", smsResponse);
return R.fail(smsResponse.getData().toString());
}
return R.ok();
}
/**
* 邮箱验证码
*
* @param email 邮箱
*/
@GetMapping("/resource/email/code")
public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
if (!mailProperties.getEnabled()) {
return R.fail("当前系统没有开启邮箱功能!");
}
SpringUtils.getAopProxy(this).emailCodeImpl(email);
return R.ok();
}
/**
* 邮箱验证码
* 独立方法避免验证码关闭之后仍然走限流
*/
@RateLimiter(key = "#email", time = 60, count = 1)
public void emailCodeImpl(String email) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
try {
MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
} catch (Exception e) {
log.error("验证码短信发送异常 => {}", e.getMessage());
throw new ServiceException(e.getMessage());
}
}
/**
* 生成验证码
*/
@GetMapping("/auth/code")
public R<CaptchaVo> getCode() {
boolean captchaEnabled = captchaProperties.getEnable();
if (!captchaEnabled) {
CaptchaVo captchaVo = new CaptchaVo();
captchaVo.setCaptchaEnabled(false);
return R.ok(captchaVo);
}
return R.ok(SpringUtils.getAopProxy(this).getCodeImpl());
}
/**
* 生成验证码
* 独立方法避免验证码关闭之后仍然走限流
*/
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
public CaptchaVo getCodeImpl() {
// 保存验证码信息
String uuid = IdUtil.simpleUUID();
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
// 生成验证码
CaptchaType captchaType = captchaProperties.getType();
boolean isMath = CaptchaType.MATH == captchaType;
Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
captcha.setGenerator(codeGenerator);
captcha.createCode();
// 如果是数学验证码使用SpEL表达式处理验证码结果
String code = captcha.getCode();
if (isMath) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
code = exp.getValue(String.class);
}
RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
CaptchaVo captchaVo = new CaptchaVo();
captchaVo.setUuid(uuid);
captchaVo.setImg(captcha.getImageBase64());
return captchaVo;
}
}

View File

@ -0,0 +1,28 @@
package org.dromara.web.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 首页
*
* @author Lion Li
*/
@SaIgnore
@RequiredArgsConstructor
@RestController
public class IndexController {
/**
* 访问首页提示语
*/
@GetMapping("/")
public String index() {
return StringUtils.format("欢迎使用{}后台管理框架,请通过前端地址访问。", SpringUtils.getApplicationName());
}
}

View File

@ -0,0 +1,25 @@
package org.dromara.web.domain.vo;
import lombok.Data;
/**
* 验证码信息
*
* @author Michelle.Chung
*/
@Data
public class CaptchaVo {
/**
* 是否开启验证码
*/
private Boolean captchaEnabled = true;
private String uuid;
/**
* 验证码图片
*/
private String img;
}

View File

@ -0,0 +1,25 @@
package org.dromara.web.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 登录租户对象
*
* @author Michelle.Chung
*/
@Data
public class LoginTenantVo {
/**
* 租户开关
*/
private Boolean tenantEnabled;
/**
* 租户对象列表
*/
private List<TenantListVo> voList;
}

View File

@ -0,0 +1,54 @@
package org.dromara.web.domain.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 登录验证信息
*
* @author Michelle.Chung
*/
@Data
public class LoginVo {
/**
* 授权令牌
*/
@JsonProperty("access_token")
private String accessToken;
/**
* 刷新令牌
*/
@JsonProperty("refresh_token")
private String refreshToken;
/**
* 授权令牌 access_token 的有效期
*/
@JsonProperty("expire_in")
private Long expireIn;
/**
* 刷新令牌 refresh_token 的有效期
*/
@JsonProperty("refresh_expire_in")
private Long refreshExpireIn;
/**
* 应用id
*/
@JsonProperty("client_id")
private String clientId;
/**
* 令牌权限
*/
private String scope;
/**
* 用户 openid
*/
private String openid;
}

View File

@ -0,0 +1,31 @@
package org.dromara.web.domain.vo;
import org.dromara.system.domain.vo.SysTenantVo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
/**
* 租户列表
*
* @author Lion Li
*/
@Data
@AutoMapper(target = SysTenantVo.class)
public class TenantListVo {
/**
* 租户编号
*/
private String tenantId;
/**
* 企业名称
*/
private String companyName;
/**
* 域名
*/
private String domain;
}

View File

@ -0,0 +1,165 @@
package org.dromara.web.listener;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.CacheConstants;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.domain.dto.UserOnlineDTO;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.ip.AddressUtils;
import org.dromara.common.log.event.LogininforEvent;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.web.service.SysLoginService;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 用户行为 侦听器的实现
*
* @author Lion Li
*/
@RequiredArgsConstructor
@Component
@Slf4j
public class UserActionListener implements SaTokenListener {
private final SaTokenConfig tokenConfig;
private final SysLoginService loginService;
/**
* 每次登录时触发
*/
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP();
UserOnlineDTO dto = new UserOnlineDTO();
dto.setIpaddr(ip);
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
dto.setBrowser(userAgent.getBrowser().getName());
dto.setOs(userAgent.getOs().getName());
dto.setLoginTime(System.currentTimeMillis());
dto.setTokenId(tokenValue);
String username = (String) loginModel.getExtra(LoginHelper.USER_NAME_KEY);
String tenantId = (String) loginModel.getExtra(LoginHelper.TENANT_KEY);
dto.setUserName(username);
dto.setClientKey((String) loginModel.getExtra(LoginHelper.CLIENT_KEY));
dto.setDeviceType(loginModel.getDevice());
dto.setDeptName((String) loginModel.getExtra(LoginHelper.DEPT_NAME_KEY));
TenantHelper.dynamic(tenantId, () -> {
if(tokenConfig.getTimeout() == -1) {
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
} else {
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(tokenConfig.getTimeout()));
}
});
// 记录登录日志
LogininforEvent logininforEvent = new LogininforEvent();
logininforEvent.setTenantId(tenantId);
logininforEvent.setUsername(username);
logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
logininforEvent.setMessage(MessageUtils.message("user.login.success"));
logininforEvent.setRequest(ServletUtils.getRequest());
SpringUtils.context().publishEvent(logininforEvent);
// 更新登录信息
loginService.recordLoginInfo((Long) loginModel.getExtra(LoginHelper.USER_KEY), ip);
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
}
/**
* 每次注销时触发
*/
@Override
public void doLogout(String loginType, Object loginId, String tokenValue) {
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue);
}
/**
* 每次被踢下线时触发
*/
@Override
public void doKickout(String loginType, Object loginId, String tokenValue) {
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue);
}
/**
* 每次被顶下线时触发
*/
@Override
public void doReplaced(String loginType, Object loginId, String tokenValue) {
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue);
}
/**
* 每次被封禁时触发
*/
@Override
public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
}
/**
* 每次被解封时触发
*/
@Override
public void doUntieDisable(String loginType, Object loginId, String service) {
}
/**
* 每次打开二级认证时触发
*/
@Override
public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
}
/**
* 每次创建Session时触发
*/
@Override
public void doCloseSafe(String loginType, String tokenValue, String service) {
}
/**
* 每次创建Session时触发
*/
@Override
public void doCreateSession(String id) {
}
/**
* 每次注销Session时触发
*/
@Override
public void doLogoutSession(String id) {
}
/**
* 每次Token续期时触发
*/
@Override
public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
}
}

View File

@ -0,0 +1,46 @@
package org.dromara.web.service;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.system.domain.SysClient;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.web.domain.vo.LoginVo;
/**
* 授权策略
*
* @author Michelle.Chung
*/
public interface IAuthStrategy {
String BASE_NAME = "AuthStrategy";
/**
* 登录
*
* @param body 登录对象
* @param client 授权管理视图对象
* @param grantType 授权类型
* @return 登录验证信息
*/
static LoginVo login(String body, SysClientVo client, String grantType) {
// 授权类型和客户端id
String beanName = grantType + BASE_NAME;
if (!SpringUtils.containsBean(beanName)) {
throw new ServiceException("授权类型不正确!");
}
IAuthStrategy instance = SpringUtils.getBean(beanName);
return instance.login(body, client);
}
/**
* 登录
*
* @param body 登录对象
* @param client 授权管理视图对象
* @return 登录验证信息
*/
LoginVo login(String body, SysClientVo client);
}

View File

@ -0,0 +1,251 @@
package org.dromara.web.service;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.lock.annotation.Lock4j;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser;
import org.dromara.common.core.constant.CacheConstants;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.constant.TenantConstants;
import org.dromara.common.core.domain.dto.PostDTO;
import org.dromara.common.core.domain.dto.RoleDTO;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.*;
import org.dromara.common.log.event.LogininforEvent;
import org.dromara.common.mybatis.helper.DataPermissionHelper;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.exception.TenantException;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.bo.SysSocialBo;
import org.dromara.system.domain.vo.*;
import org.dromara.system.mapper.SysUserMapper;
import org.dromara.system.service.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Date;
import java.util.List;
import java.util.function.Supplier;
/**
* 登录校验方法
*
* @author Lion Li
*/
@RequiredArgsConstructor
@Slf4j
@Service
public class SysLoginService {
@Value("${user.password.maxRetryCount}")
private Integer maxRetryCount;
@Value("${user.password.lockTime}")
private Integer lockTime;
private final ISysTenantService tenantService;
private final ISysPermissionService permissionService;
private final ISysSocialService sysSocialService;
private final ISysRoleService roleService;
private final ISysDeptService deptService;
private final ISysPostService postService;
private final SysUserMapper userMapper;
/**
* 绑定第三方用户
*
* @param authUserData 授权响应实体
*/
@Lock4j
public void socialRegister(AuthUser authUserData) {
String authId = authUserData.getSource() + authUserData.getUuid();
// 第三方用户信息
SysSocialBo bo = BeanUtil.toBean(authUserData, SysSocialBo.class);
BeanUtil.copyProperties(authUserData.getToken(), bo);
Long userId = LoginHelper.getUserId();
bo.setUserId(userId);
bo.setAuthId(authId);
bo.setOpenId(authUserData.getUuid());
bo.setUserName(authUserData.getUsername());
bo.setNickName(authUserData.getNickname());
List<SysSocialVo> checkList = sysSocialService.selectByAuthId(authId);
if (CollUtil.isNotEmpty(checkList)) {
throw new ServiceException("此三方账号已经被绑定!");
}
// 查询是否已经绑定用户
SysSocialBo params = new SysSocialBo();
params.setUserId(userId);
params.setSource(bo.getSource());
List<SysSocialVo> list = sysSocialService.queryList(params);
if (CollUtil.isEmpty(list)) {
// 没有绑定用户, 新增用户信息
sysSocialService.insertByBo(bo);
} else {
// 更新用户信息
bo.setId(list.get(0).getId());
sysSocialService.updateByBo(bo);
// 如果要绑定的平台账号已经被绑定过了 是否抛异常自行决断
// throw new ServiceException("此平台账号已经被绑定!");
}
}
/**
* 退出登录
*/
public void logout() {
try {
LoginUser loginUser = LoginHelper.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
return;
}
if (TenantHelper.isEnable() && LoginHelper.isSuperAdmin()) {
// 超级管理员 登出清除动态租户
TenantHelper.clearDynamic();
}
recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
} catch (NotLoginException ignored) {
} finally {
try {
StpUtil.logout();
} catch (NotLoginException ignored) {
}
}
}
/**
* 记录登录信息
*
* @param tenantId 租户ID
* @param username 用户名
* @param status 状态
* @param message 消息内容
*/
public void recordLogininfor(String tenantId, String username, String status, String message) {
LogininforEvent logininforEvent = new LogininforEvent();
logininforEvent.setTenantId(tenantId);
logininforEvent.setUsername(username);
logininforEvent.setStatus(status);
logininforEvent.setMessage(message);
logininforEvent.setRequest(ServletUtils.getRequest());
SpringUtils.context().publishEvent(logininforEvent);
}
/**
* 构建登录用户
*/
public LoginUser buildLoginUser(SysUserVo user) {
LoginUser loginUser = new LoginUser();
Long userId = user.getUserId();
loginUser.setTenantId(user.getTenantId());
loginUser.setUserId(userId);
loginUser.setDeptId(user.getDeptId());
loginUser.setUsername(user.getUserName());
loginUser.setNickname(user.getNickName());
loginUser.setUserType(user.getUserType());
loginUser.setMenuPermission(permissionService.getMenuPermission(userId));
loginUser.setRolePermission(permissionService.getRolePermission(userId));
if (ObjectUtil.isNotNull(user.getDeptId())) {
Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById);
loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY));
loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY));
}
List<SysRoleVo> roles = roleService.selectRolesByUserId(userId);
List<SysPostVo> posts = postService.selectPostsByUserId(userId);
loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
loginUser.setPosts(BeanUtil.copyToList(posts, PostDTO.class));
return loginUser;
}
/**
* 记录登录信息
*
* @param userId 用户ID
*/
public void recordLoginInfo(Long userId, String ip) {
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(ip);
sysUser.setLoginDate(DateUtils.getNowDate());
sysUser.setUpdateBy(userId);
DataPermissionHelper.ignore(() -> userMapper.updateById(sysUser));
}
/**
* 登录校验
*/
public void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
String loginFail = Constants.LOGIN_FAIL;
// 获取用户登录错误次数默认为0 (可自定义限制策略 例如: key + username + ip)
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
// 锁定时间内登录 则踢出
if (errorNumber >= maxRetryCount) {
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
}
if (supplier.get()) {
// 错误次数递增
errorNumber++;
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
// 达到规定错误次数 则锁定登录
if (errorNumber >= maxRetryCount) {
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
} else {
// 未达到规定错误次数
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
}
}
// 登录成功 清空错误次数
RedisUtils.deleteObject(errorKey);
}
/**
* 校验租户
*
* @param tenantId 租户ID
*/
public void checkTenant(String tenantId) {
if (!TenantHelper.isEnable()) {
return;
}
if (StringUtils.isBlank(tenantId)) {
throw new TenantException("tenant.number.not.blank");
}
if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
return;
}
SysTenantVo tenant = tenantService.queryByTenantId(tenantId);
if (ObjectUtil.isNull(tenant)) {
log.info("登录租户:{} 不存在.", tenantId);
throw new TenantException("tenant.not.exists");
} else if (SystemConstants.DISABLE.equals(tenant.getStatus())) {
log.info("登录租户:{} 已被停用.", tenantId);
throw new TenantException("tenant.blocked");
} else if (ObjectUtil.isNotNull(tenant.getExpireTime())
&& new Date().after(tenant.getExpireTime())) {
log.info("登录租户:{} 已超过有效期.", tenantId);
throw new TenantException("tenant.expired");
}
}
}

View File

@ -0,0 +1,115 @@
package org.dromara.web.service;
import cn.dev33.satoken.secure.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.domain.model.RegisterBody;
import org.dromara.common.core.enums.UserType;
import org.dromara.common.core.exception.user.CaptchaException;
import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.log.event.LogininforEvent;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.bo.SysUserBo;
import org.dromara.system.mapper.SysUserMapper;
import org.dromara.system.service.ISysUserService;
import org.springframework.stereotype.Service;
/**
* 注册校验方法
*
* @author Lion Li
*/
@RequiredArgsConstructor
@Service
public class SysRegisterService {
private final ISysUserService userService;
private final SysUserMapper userMapper;
private final CaptchaProperties captchaProperties;
/**
* 注册
*/
public void register(RegisterBody registerBody) {
String tenantId = registerBody.getTenantId();
String username = registerBody.getUsername();
String password = registerBody.getPassword();
// 校验用户类型是否存在
String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关
if (captchaEnabled) {
validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid());
}
SysUserBo sysUser = new SysUserBo();
sysUser.setUserName(username);
sysUser.setNickName(username);
sysUser.setPassword(BCrypt.hashpw(password));
sysUser.setUserType(userType);
boolean exist = TenantHelper.dynamic(tenantId, () -> {
return userMapper.exists(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUserName, sysUser.getUserName()));
});
if (exist) {
throw new UserException("user.register.save.error", username);
}
boolean regFlag = userService.registerUser(sysUser, tenantId);
if (!regFlag) {
throw new UserException("user.register.error");
}
recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success"));
}
/**
* 校验验证码
*
* @param username 用户名
* @param code 验证码
* @param uuid 唯一标识
*/
public void validateCaptcha(String tenantId, String username, String code, String uuid) {
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
String captcha = RedisUtils.getCacheObject(verifyKey);
RedisUtils.deleteObject(verifyKey);
if (captcha == null) {
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha)) {
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException();
}
}
/**
* 记录登录信息
*
* @param tenantId 租户ID
* @param username 用户名
* @param status 状态
* @param message 消息内容
* @return
*/
private void recordLogininfor(String tenantId, String username, String status, String message) {
LogininforEvent logininforEvent = new LogininforEvent();
logininforEvent.setTenantId(tenantId);
logininforEvent.setUsername(username);
logininforEvent.setStatus(status);
logininforEvent.setMessage(message);
logininforEvent.setRequest(ServletUtils.getRequest());
SpringUtils.context().publishEvent(logininforEvent);
}
}

View File

@ -0,0 +1,102 @@
package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.EmailLoginBody;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.mapper.SysUserMapper;
import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.springframework.stereotype.Service;
/**
* 邮件认证策略
*
* @author Michelle.Chung
*/
@Slf4j
@Service("email" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class EmailAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService;
private final SysUserMapper userMapper;
@Override
public LoginVo login(String body, SysClientVo client) {
EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
ValidatorUtils.validate(loginBody);
String tenantId = loginBody.getTenantId();
String email = loginBody.getEmail();
String emailCode = loginBody.getEmailCode();
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByEmail(email);
loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
return loginVo;
}
/**
* 校验邮箱验证码
*/
private boolean validateEmailCode(String tenantId, String email, String emailCode) {
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
if (StringUtils.isBlank(code)) {
loginService.recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException();
}
return code.equals(emailCode);
}
private SysUserVo loadUserByEmail(String email) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, email));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", email);
throw new UserException("user.not.exists", email);
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", email);
throw new UserException("user.blocked", email);
}
return user;
}
}

View File

@ -0,0 +1,123 @@
package org.dromara.web.service.impl;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.PasswordLoginBody;
import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.exception.user.CaptchaException;
import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.mapper.SysUserMapper;
import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.springframework.stereotype.Service;
/**
* 密码认证策略
*
* @author Michelle.Chung
*/
@Slf4j
@Service("password" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class PasswordAuthStrategy implements IAuthStrategy {
private final CaptchaProperties captchaProperties;
private final SysLoginService loginService;
private final SysUserMapper userMapper;
@Override
public LoginVo login(String body, SysClientVo client) {
PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
ValidatorUtils.validate(loginBody);
String tenantId = loginBody.getTenantId();
String username = loginBody.getUsername();
String password = loginBody.getPassword();
String code = loginBody.getCode();
String uuid = loginBody.getUuid();
boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关
if (captchaEnabled) {
validateCaptcha(tenantId, username, code, uuid);
}
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByUsername(username);
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
// 此处可根据登录用户的数据不同 自行创建 loginUser
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
return loginVo;
}
/**
* 校验验证码
*
* @param username 用户名
* @param code 验证码
* @param uuid 唯一标识
*/
private void validateCaptcha(String tenantId, String username, String code, String uuid) {
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
String captcha = RedisUtils.getCacheObject(verifyKey);
RedisUtils.deleteObject(verifyKey);
if (captcha == null) {
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha)) {
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException();
}
}
private SysUserVo loadUserByUsername(String username) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new UserException("user.not.exists", username);
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new UserException("user.blocked", username);
}
return user;
}
}

View File

@ -0,0 +1,102 @@
package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.SmsLoginBody;
import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.mapper.SysUserMapper;
import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.springframework.stereotype.Service;
/**
* 短信认证策略
*
* @author Michelle.Chung
*/
@Slf4j
@Service("sms" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class SmsAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService;
private final SysUserMapper userMapper;
@Override
public LoginVo login(String body, SysClientVo client) {
SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
ValidatorUtils.validate(loginBody);
String tenantId = loginBody.getTenantId();
String phonenumber = loginBody.getPhonenumber();
String smsCode = loginBody.getSmsCode();
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByPhonenumber(phonenumber);
loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
return loginVo;
}
/**
* 校验短信验证码
*/
private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) {
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
if (StringUtils.isBlank(code)) {
loginService.recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException();
}
return code.equals(smsCode);
}
private SysUserVo loadUserByPhonenumber(String phonenumber) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, phonenumber));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", phonenumber);
throw new UserException("user.not.exists", phonenumber);
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", phonenumber);
throw new UserException("user.blocked", phonenumber);
}
return user;
}
}

View File

@ -0,0 +1,131 @@
package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.Method;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.SocialLoginBody;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.utils.SocialUtils;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysSocialVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.mapper.SysUserMapper;
import org.dromara.system.service.ISysSocialService;
import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* 第三方授权策略
*
* @author thiszhc is 三三
*/
@Slf4j
@Service("social" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class SocialAuthStrategy implements IAuthStrategy {
private final SocialProperties socialProperties;
private final ISysSocialService sysSocialService;
private final SysUserMapper userMapper;
private final SysLoginService loginService;
/**
* 登录-第三方授权登录
*
* @param body 登录信息
* @param client 客户端信息
*/
@Override
public LoginVo login(String body, SysClientVo client) {
SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
ValidatorUtils.validate(loginBody);
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
loginBody.getSource(), loginBody.getSocialCode(),
loginBody.getSocialState(), socialProperties);
if (!response.ok()) {
throw new ServiceException(response.getMsg());
}
AuthUser authUserData = response.getData();
if ("GITEE".equals(authUserData.getSource())) {
// 如用户使用 gitee 登录顺手 star 给作者一点支持 拒绝白嫖
HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Vue-Plus")
.formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
.executeAsync();
HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Cloud-Plus")
.formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
.executeAsync();
}
List<SysSocialVo> list = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
if (CollUtil.isEmpty(list)) {
throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!");
}
SysSocialVo social;
if (TenantHelper.isEnable()) {
Optional<SysSocialVo> opt = StreamUtils.findAny(list, x -> x.getTenantId().equals(loginBody.getTenantId()));
if (opt.isEmpty()) {
throw new ServiceException("对不起,你没有权限登录当前租户!");
}
social = opt.get();
} else {
social = list.get(0);
}
LoginUser loginUser = TenantHelper.dynamic(social.getTenantId(), () -> {
SysUserVo user = loadUser(social.getUserId());
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
return loginVo;
}
private SysUserVo loadUser(Long userId) {
SysUserVo user = userMapper.selectVoById(userId);
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", "");
throw new UserException("user.not.exists", "");
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", "");
throw new UserException("user.blocked", "");
}
return user;
}
}

View File

@ -0,0 +1,111 @@
package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.request.AuthWechatMiniProgramRequest;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.XcxLoginBody;
import org.dromara.common.core.domain.model.XcxLoginUser;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.springframework.stereotype.Service;
/**
* 小程序认证策略
*
* @author Michelle.Chung
*/
@Slf4j
@Service("xcx" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class XcxAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService;
@Override
public LoginVo login(String body, SysClientVo client) {
XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);
ValidatorUtils.validate(loginBody);
// xcxCode 小程序调用 wx.login 授权后获取
String xcxCode = loginBody.getXcxCode();
// 多个小程序识别使用
String appid = loginBody.getAppid();
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key openid
AuthRequest authRequest = new AuthWechatMiniProgramRequest(AuthConfig.builder()
.clientId(appid).clientSecret("自行填写密钥 可根据不同appid填入不同密钥")
.ignoreCheckRedirectUri(true).ignoreCheckState(true).build());
AuthCallback authCallback = new AuthCallback();
authCallback.setCode(xcxCode);
AuthResponse<AuthUser> resp = authRequest.login(authCallback);
String openid, unionId;
if (resp.ok()) {
AuthToken token = resp.getData().getToken();
openid = token.getOpenId();
// 微信小程序只有关联到微信开放平台下之后才能获取到 unionId因此unionId不一定能返回
unionId = token.getUnionId();
} else {
throw new ServiceException(resp.getMsg());
}
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
SysUserVo user = loadUserByOpenid(openid);
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
XcxLoginUser loginUser = new XcxLoginUser();
loginUser.setTenantId(user.getTenantId());
loginUser.setUserId(user.getUserId());
loginUser.setUsername(user.getUserName());
loginUser.setNickname(user.getNickName());
loginUser.setUserType(user.getUserType());
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
loginUser.setOpenid(openid);
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
loginVo.setOpenid(openid);
return loginVo;
}
private SysUserVo loadUserByOpenid(String openid) {
// 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
// todo 自行实现 userService.selectUserByOpenid(openid);
SysUserVo user = new SysUserVo();
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", openid);
// todo 用户不存在 业务逻辑自行实现
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", openid);
// todo 用户已被停用 业务逻辑自行实现
}
return user;
}
}

View File

@ -0,0 +1,265 @@
--- # 监控中心配置
spring.boot.admin.client:
# 增加客户端开关
enabled: false
url: http://localhost:9090/admin
instance:
service-host-type: IP
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
username: @monitor.username@
password: @monitor.password@
--- # snail-job 配置
snail-job:
enabled: false
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
group: "ruoyi_group"
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
server:
host: 127.0.0.1
port: 17888
# 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段
namespace: ${spring.profiles.active}
# 随主应用端口漂移
port: 2${server.port}
# 客户端ip指定
host:
# RPC类型: netty, grpc
rpc-type: grpc
--- # 数据源配置
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
dynamic:
# 性能分析插件(有性能损耗 不建议生产环境使用)
p6spy: true
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式 匹配不到数据源则报错
strict: true
datasource:
# 主库数据源
master:
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
url: jdbc:mysql://111.229.149.206:3306/ruoyi?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: ruoyi
password: 4yHmmhKyRYNWeRWk
# # 从库数据源
# slave:
# lazy: true
# type: ${spring.datasource.type}
# driverClassName: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
# username:
# password:
# oracle:
# type: ${spring.datasource.type}
# driverClassName: oracle.jdbc.OracleDriver
# url: jdbc:oracle:thin:@//localhost:1521/XE
# username: ROOT
# password: root
# postgres:
# type: ${spring.datasource.type}
# driverClassName: org.postgresql.Driver
# url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
# username: root
# password: root
# sqlserver:
# type: ${spring.datasource.type}
# driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
# url: jdbc:sqlserver://localhost:1433;DatabaseName=tempdb;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true
# username: SA
# password: root
hikari:
# 最大连接池数量
maxPoolSize: 20
# 最小空闲线程数量
minIdle: 10
# 配置获取连接等待超时的时间
connectionTimeout: 30000
# 校验超时时间
validationTimeout: 5000
# 空闲连接存活最大时间默认10分钟
idleTimeout: 600000
# 此属性控制池中连接的最长生命周期值0表示无限生命周期默认30分钟
maxLifetime: 1800000
# 多久检查一次连接的活性
keepaliveTime: 30000
--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
spring.data:
redis:
# 地址
host: localhost
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# redis 密码必须配置
# password: ''
# 连接超时时间
timeout: 10s
# 是否开启ssl
ssl.enabled: false
# redisson 配置
redisson:
# redis key前缀
keyPrefix:
# 线程池数量
threads: 4
# Netty线程池数量
nettyThreads: 8
# 单节点配置
singleServerConfig:
# 客户端名称 不能用中文
clientName: RuoYi-Vue-Plus
# 最小空闲连接数
connectionMinimumIdleSize: 8
# 连接池大小
connectionPoolSize: 32
# 连接空闲超时,单位:毫秒
idleConnectionTimeout: 10000
# 命令等待超时,单位:毫秒
timeout: 3000
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
--- # mail 邮件发送
mail:
enabled: false
host: smtp.163.com
port: 465
# 是否需要用户名密码验证
auth: true
# 发送方遵循RFC-822标准
from: xxx@163.com
# 用户名注意如果使用foxmail邮箱此处user为qq号
user: xxx@163.com
# 密码注意某些邮箱需要为SMTP服务单独设置密码详情查看相关帮助
pass: xxxxxxxxxx
# 使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展。
starttlsEnable: true
# 使用SSL安全连接
sslEnable: true
# SMTP超时时长单位毫秒缺省值不超时
timeout: 0
# Socket连接超时值单位毫秒缺省值不超时
connectionTimeout: 0
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
sms:
# 配置源类型用于标定配置来源(interface,yaml)
config-type: yaml
# 用于标定yml中的配置是否开启短信拦截接口配置不受此限制
restricted: true
# 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
minute-max: 1
# 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
account-max: 30
# 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
blends:
# 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
# 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户
config1:
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: alibaba
# 有些称为accessKey有些称之为apiKey也有称为sdkKey或者appId。
access-key-id: 您的accessKey
# 称为accessSecret有些称之为apiSecret
access-key-secret: 您的accessKeySecret
signature: 您的短信签名
sdk-app-id: 您的sdkAppId
config2:
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: tencent
access-key-id: 您的accessKey
access-key-secret: 您的accessKeySecret
signature: 您的短信签名
sdk-app-id: 您的sdkAppId
--- # 三方授权
justauth:
# 前端外网访问地址
address: http://localhost:80
type:
maxkey:
# maxkey 服务器地址
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
server-url: http://sso.maxkey.top
client-id: 876892492581044224
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
redirect-uri: ${justauth.address}/social-callback?source=maxkey
topiam:
# topiam 服务器地址
server-url: http://127.0.0.1:1898/api/v1/authorize/y0q************spq***********8ol
client-id: 449c4*********937************759
client-secret: ac7***********1e0************28d
redirect-uri: ${justauth.address}/social-callback?source=topiam
scopes: [openid, email, phone, profile]
qq:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=qq
union-id: false
weibo:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=weibo
gitee:
client-id: 91436b7940090d09c72c7daf85b959cfd5f215d67eea73acbf61b6b590751a98
client-secret: 02c6fcfd70342980cd8dd2f2c06c1a350645d76c754d7a264c4e125f9ba915ac
redirect-uri: ${justauth.address}/social-callback?source=gitee
dingtalk:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
baidu:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=baidu
csdn:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=csdn
coding:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=coding
coding-group-name: xx
oschina:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=oschina
alipay_wallet:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
alipay-public-key: MIIB**************DAQAB
wechat_open:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
wechat_mp:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
wechat_enterprise:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
agent-id: 1000002
gitlab:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitlab

View File

@ -0,0 +1,267 @@
--- # 临时文件存储位置 避免临时文件被系统清理报错
spring.servlet.multipart.location: /ruoyi/server/temp
--- # 监控中心配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
url: http://localhost:9090/admin
instance:
service-host-type: IP
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
username: @monitor.username@
password: @monitor.password@
--- # snail-job 配置
snail-job:
enabled: true
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
group: "ruoyi_group"
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config`表
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
server:
host: 127.0.0.1
port: 17888
# 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段
namespace: ${spring.profiles.active}
# 随主应用端口漂移
port: 2${server.port}
# 客户端ip指定
host:
# RPC类型: netty, grpc
rpc-type: grpc
--- # 数据源配置
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
dynamic:
# 性能分析插件(有性能损耗 不建议生产环境使用)
p6spy: false
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式 匹配不到数据源则报错
strict: true
datasource:
# 主库数据源
master:
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: root
# # 从库数据源
# slave:
# lazy: true
# type: ${spring.datasource.type}
# driverClassName: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
# username:
# password:
# oracle:
# type: ${spring.datasource.type}
# driverClassName: oracle.jdbc.OracleDriver
# url: jdbc:oracle:thin:@//localhost:1521/XE
# username: ROOT
# password: root
# postgres:
# type: ${spring.datasource.type}
# driverClassName: org.postgresql.Driver
# url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
# username: root
# password: root
# sqlserver:
# type: ${spring.datasource.type}
# driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
# url: jdbc:sqlserver://localhost:1433;DatabaseName=tempdb;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true
# username: SA
# password: root
hikari:
# 最大连接池数量
maxPoolSize: 20
# 最小空闲线程数量
minIdle: 10
# 配置获取连接等待超时的时间
connectionTimeout: 30000
# 校验超时时间
validationTimeout: 5000
# 空闲连接存活最大时间默认10分钟
idleTimeout: 600000
# 此属性控制池中连接的最长生命周期值0表示无限生命周期默认30分钟
maxLifetime: 1800000
# 多久检查一次连接的活性
keepaliveTime: 30000
--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
spring.data:
redis:
# 地址
host: localhost
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# redis 密码必须配置
password: ruoyi123
# 连接超时时间
timeout: 10s
# 是否开启ssl
ssl.enabled: false
# redisson 配置
redisson:
# redis key前缀
keyPrefix:
# 线程池数量
threads: 16
# Netty线程池数量
nettyThreads: 32
# 单节点配置
singleServerConfig:
# 客户端名称 不能用中文
clientName: RuoYi-Vue-Plus
# 最小空闲连接数
connectionMinimumIdleSize: 32
# 连接池大小
connectionPoolSize: 64
# 连接空闲超时,单位:毫秒
idleConnectionTimeout: 10000
# 命令等待超时,单位:毫秒
timeout: 3000
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
--- # mail 邮件发送
mail:
enabled: false
host: smtp.163.com
port: 465
# 是否需要用户名密码验证
auth: true
# 发送方遵循RFC-822标准
from: xxx@163.com
# 用户名注意如果使用foxmail邮箱此处user为qq号
user: xxx@163.com
# 密码注意某些邮箱需要为SMTP服务单独设置密码详情查看相关帮助
pass: xxxxxxxxxx
# 使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展。
starttlsEnable: true
# 使用SSL安全连接
sslEnable: true
# SMTP超时时长单位毫秒缺省值不超时
timeout: 0
# Socket连接超时值单位毫秒缺省值不超时
connectionTimeout: 0
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
sms:
# 配置源类型用于标定配置来源(interface,yaml)
config-type: yaml
# 用于标定yml中的配置是否开启短信拦截接口配置不受此限制
restricted: true
# 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
minute-max: 1
# 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
account-max: 30
# 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
blends:
# 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
# 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号 也可用于区分租户
config1:
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: alibaba
# 有些称为accessKey有些称之为apiKey也有称为sdkKey或者appId。
access-key-id: 您的accessKey
# 称为accessSecret有些称之为apiSecret
access-key-secret: 您的accessKeySecret
signature: 您的短信签名
sdk-app-id: 您的sdkAppId
config2:
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: tencent
access-key-id: 您的accessKey
access-key-secret: 您的accessKeySecret
signature: 您的短信签名
sdk-app-id: 您的sdkAppId
--- # 三方授权
justauth:
# 前端外网访问地址
address: http://localhost:80
type:
maxkey:
# maxkey 服务器地址
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
server-url: http://sso.maxkey.top
client-id: 876892492581044224
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
redirect-uri: ${justauth.address}/social-callback?source=maxkey
topiam:
# topiam 服务器地址
server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol
client-id: 449c4*********937************759
client-secret: ac7***********1e0************28d
redirect-uri: ${justauth.address}/social-callback?source=topiam
scopes: [ openid, email, phone, profile ]
qq:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=qq
union-id: false
weibo:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=weibo
gitee:
client-id: 91436b7940090d09c72c7daf85b959cfd5f215d67eea73acbf61b6b590751a98
client-secret: 02c6fcfd70342980cd8dd2f2c06c1a350645d76c754d7a264c4e125f9ba915ac
redirect-uri: ${justauth.address}/social-callback?source=gitee
dingtalk:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
baidu:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=baidu
csdn:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=csdn
coding:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=coding
coding-group-name: xx
oschina:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=oschina
alipay_wallet:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
alipay-public-key: MIIB**************DAQAB
wechat_open:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
wechat_mp:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
wechat_enterprise:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
agent-id: 1000002
gitlab:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitlab

View File

@ -0,0 +1,283 @@
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 8080
servlet:
# 应用的访问路径
context-path: /
# undertow 配置
undertow:
# HTTP post内容的最大大小。当值为-1时默认值为大小是无限的
max-http-post-size: -1
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 512
# 是否分配的直接内存
direct-buffers: true
threads:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
io: 8
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
worker: 256
captcha:
enable: true
# 页面 <参数设置> 可开启关闭 验证码校验
# 验证码类型 math 数组计算 char 字符验证
type: MATH
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
category: CIRCLE
# 数字验证码位数
numberLength: 1
# 字符验证码长度
charLength: 4
# 日志配置
logging:
level:
org.dromara: @logging.level@
org.springframework: warn
org.mybatis.spring.mapper: error
org.apache.fury: warn
config: classpath:logback-plus.xml
# 用户配置
user:
password:
# 密码最大错误次数
maxRetryCount: 5
# 密码锁定时间默认10分钟
lockTime: 10
# Spring配置
spring:
application:
name: RuoYi-Vue-Plus
threads:
# 开启虚拟线程 仅jdk21可用
virtual:
enabled: false
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
profiles:
active: @profiles.active@
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
# 设置总上传的文件大小
max-request-size: 20MB
mvc:
# 设置静态资源路径 防止所有请求都去查静态资源
static-path-pattern: /static/**
format:
date-time: yyyy-MM-dd HH:mm:ss
jackson:
# 日期格式化
date-format: yyyy-MM-dd HH:mm:ss
serialization:
# 格式化输出
indent_output: false
# 忽略无法转换的对象
fail_on_empty_beans: false
deserialization:
# 允许对象忽略json中不存在的属性
fail_on_unknown_properties: false
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: false
# jwt秘钥
jwt-secret-key: abcdefghijklmnopqrstuvwxyz
# security配置
security:
# 排除路径
excludes:
- /*.html
- /**/*.html
- /**/*.css
- /**/*.js
- /favicon.ico
- /error
- /*/api-docs
- /*/api-docs/**
- /warm-flow-ui/token-name
# 多租户配置
tenant:
# 是否开启
enable: true
# 排除表
excludes:
- sys_menu
- sys_tenant
- sys_tenant_package
- sys_role_dept
- sys_role_menu
- sys_user_post
- sys_user_role
- sys_client
- sys_oss_config
# MyBatisPlus配置
# https://baomidou.com/config/
mybatis-plus:
# 自定义配置 是否全局开启逻辑删除 关闭后 所有逻辑删除功能将失效
enableLogicDelete: true
# 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper
mapperPackage: org.dromara.**.mapper
# 对应的 XML 文件位置
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 实体扫描多个package用逗号或者分号分隔
typeAliasesPackage: org.dromara.**.domain
global-config:
dbConfig:
# 主键类型
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
# 如需改为自增 需要将数据库表全部设置为自增
idType: ASSIGN_ID
# 数据加密
mybatis-encryptor:
# 是否开启加密
enable: false
# 默认加密算法
algorithm: BASE64
# 编码方式 BASE64/HEX。默认BASE64
encode: BASE64
# 安全秘钥 对称算法的秘钥 如AESSM4
password:
# 公私钥 非对称算法的公私钥 如SM2RSA
publicKey:
privateKey:
# api接口加密
api-decrypt:
# 是否开启全局接口加密
enabled: true
# AES 加密头标识
headerFlag: encrypt-key
# 响应加密公钥 非对称算法的公私钥 如SM2RSA 使用者请自行更换
# 对应前端解密私钥 MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJnNwrj4hi/y3CCJu868ghCG5dUj8wZK++RNlTLcXoMmdZWEQ/u02RgD5LyLAXGjLOjbMtC+/J9qofpSGTKSx/MCAwEAAQ==
# 请求解密私钥 非对称算法的公私钥 如SM2RSA 使用者请自行更换
# 对应前端加密公钥 MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
springdoc:
api-docs:
# 是否开启接口文档
enabled: true
# swagger-ui:
# # 持久化认证数据
# persistAuthorization: true
info:
# 标题
title: '标题RuoYi-Vue-Plus多租户管理系统_接口文档'
# 描述
description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
# 版本
version: '版本号: ${ruoyi.version}'
# 作者信息
contact:
name: Lion Li
email: crazylionli@163.com
url: https://gitee.com/dromara/RuoYi-Vue-Plus
components:
# 鉴权方式配置
security-schemes:
apiKey:
type: APIKEY
in: HEADER
name: ${sa-token.token-name}
#这里定义了两个分组,可定义多个,也可以不定义
group-configs:
- group: 1.演示模块
packages-to-scan: org.dromara.demo
- group: 2.通用模块
packages-to-scan: org.dromara.web
- group: 3.系统模块
packages-to-scan: org.dromara.system
- group: 4.代码生成模块
packages-to-scan: org.dromara.generator
- group: 5.工作流模块
packages-to-scan: org.dromara.workflow
- group: 6.PMS民宿管理模块
packages-to-scan: org.dromara.pms
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludeUrls:
- /system/notice
# 全局线程池相关配置
# 如使用JDK21请直接使用虚拟线程 不要开启此配置
thread-pool:
# 是否开启线程池
enabled: false
# 队列最大长度
queueCapacity: 128
# 线程池维护线程所允许的空闲时间
keepAliveSeconds: 300
--- # 分布式锁 lock4j 全局配置
lock4j:
# 获取分布式锁超时时间,默认为 3000 毫秒
acquire-timeout: 3000
# 分布式锁的超时时间,默认为 30 秒
expire: 30000
--- # Actuator 监控端点的配置项
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
logfile:
external-file: ./logs/sys-console.log
--- # 默认/推荐使用sse推送
sse:
enabled: true
path: /resource/sse
--- # websocket
websocket:
# 如果关闭 需要和前端开关一起关闭
enabled: false
# 路径
path: /resource/websocket
# 设置访问源地址
allowedOrigins: '*'
--- # warm-flow工作流配置
warm-flow:
# 是否开启工作流默认true
enabled: true
# 是否开启设计器ui
ui: true
# 默认Authorization如果有多个token用逗号分隔
token-name: ${sa-token.token-name},clientid
# 流程状态对应的三元色
chart-status-color:
## 未办理
- 62,62,62
## 待办理
- 255,205,23
## 已办理
- 157,255,0

View File

@ -0,0 +1,8 @@
Application Version: ${revision}
Spring Boot Version: ${spring-boot.version}
__________ _____.___.__ ____ ____ __________.__
\______ \__ __ ____\__ | |__| \ \ / /_ __ ____ \______ \ | __ __ ______
| _/ | \/ _ \/ | | | ______ \ Y / | \_/ __ \ ______ | ___/ | | | \/ ___/
| | \ | ( <_> )____ | | /_____/ \ /| | /\ ___/ /_____/ | | | |_| | /\___ \
|____|_ /____/ \____// ______|__| \___/ |____/ \___ > |____| |____/____//____ >
\/ \/ \/ \/

View File

@ -0,0 +1,61 @@
#错误消息
not.null=* 必须填写
user.jcaptcha.error=验证码错误
user.jcaptcha.expire=验证码已失效
user.not.exists=对不起, 您的账号:{0} 不存在.
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
user.password.delete=对不起,您的账号:{0} 已被删除
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
role.blocked=角色已封禁,请联系管理员
user.logout.success=退出成功
length.not.valid=长度必须在{min}到{max}个字符之间
user.username.not.blank=用户名不能为空
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成且必须以非数字开头
user.username.length.valid=账户长度必须在{min}到{max}个字符之间
user.password.not.blank=用户密码不能为空
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
user.password.not.valid=* 5-50个字符
user.email.not.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空
user.phonenumber.not.blank=用户手机号不能为空
user.mobile.phone.number.not.valid=手机号格式错误
user.login.success=登录成功
user.register.success=注册成功
user.register.save.error=保存用户 {0} 失败,注册账号已存在
user.register.error=注册失败,请联系系统管理人员
user.notfound=请重新登录
user.forcelogout=管理员强制退出,请重新登录
user.unknown.error=未知错误,请重新登录
auth.grant.type.error=认证权限类型错误
auth.grant.type.blocked=认证权限类型已禁用
auth.grant.type.not.blank=认证权限类型不能为空
auth.clientid.not.blank=认证客户端id不能为空
##文件上传消息
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB
upload.filename.exceed.length=上传的文件名最长{0}个字符
##权限
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
repeat.submit.message=不允许重复提交,请稍候再试
rate.limiter.message=访问过于频繁,请稍候再试
sms.code.not.blank=短信验证码不能为空
sms.code.retry.limit.count=短信验证码输入错误{0}次
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
email.code.not.blank=邮箱验证码不能为空
email.code.retry.limit.count=邮箱验证码输入错误{0}次
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟
xcx.code.not.blank=小程序[code]不能为空
social.source.not.blank=第三方登录平台[source]不能为空
social.code.not.blank=第三方登录平台[code]不能为空
social.state.not.blank=第三方登录平台[state]不能为空
##租户
tenant.number.not.blank=租户编号不能为空
tenant.not.exists=对不起, 您的租户不存在,请联系管理员
tenant.blocked=对不起,您的租户已禁用,请联系管理员
tenant.expired=对不起,您的租户已过期,请联系管理员

View File

@ -0,0 +1,61 @@
#错误消息
not.null=* Required fill in
user.jcaptcha.error=Captcha error
user.jcaptcha.expire=Captcha invalid
user.not.exists=Sorry, your account: {0} does not exist
user.password.not.match=User does not exist/Password error
user.password.retry.limit.count=Password input error {0} times
user.password.retry.limit.exceed=Password input error {0} times, account locked for {1} minutes
user.password.delete=Sorry, your account{0} has been deleted
user.blocked=Sorry, your account: {0} has been disabled. Please contact the administrator
role.blocked=Role disabledplease contact administrators
user.logout.success=Exit successful
length.not.valid=The length must be between {min} and {max} characters
user.username.not.blank=Username cannot be blank
user.username.not.valid=* 2 to 20 chinese characters, letters, numbers or underscores, and must start with a non number
user.username.length.valid=Account length must be between {min} and {max} characters
user.password.not.blank=Password cannot be empty
user.password.length.valid=Password length must be between {min} and {max} characters
user.password.not.valid=* 5-50 characters
user.email.not.valid=Mailbox format error
user.email.not.blank=Mailbox cannot be blank
user.phonenumber.not.blank=Phone number cannot be blank
user.mobile.phone.number.not.valid=Phone number format error
user.login.success=Login successful
user.register.success=Register successful
user.register.save.error=Failed to save user {0}, The registered account already exists
user.register.error=Register failed, please contact system administrator
user.notfound=Please login again
user.forcelogout=The administrator is forced to exitplease login again
user.unknown.error=Unknown error, please login again
auth.grant.type.error=Auth grant type error
auth.grant.type.blocked=Auth grant type disabled
auth.grant.type.not.blank=Auth grant type cannot be blank
auth.clientid.not.blank=Auth clientid cannot be blank
##文件上传消息
upload.exceed.maxSize=The uploaded file size exceeds the limit file size<br/>the maximum allowed file size is{0}MB
upload.filename.exceed.length=The maximum length of uploaded file name is {0} characters
##权限
no.permission=You do not have permission to the dataplease contact your administrator to add permissions [{0}]
no.create.permission=You do not have permission to create dataplease contact your administrator to add permissions [{0}]
no.update.permission=You do not have permission to modify dataplease contact your administrator to add permissions [{0}]
no.delete.permission=You do not have permission to delete dataplease contact your administrator to add permissions [{0}]
no.export.permission=You do not have permission to export dataplease contact your administrator to add permissions [{0}]
no.view.permission=You do not have permission to view dataplease contact your administrator to add permissions [{0}]
repeat.submit.message=Repeat submit is not allowed, please try again later
rate.limiter.message=Visit too frequently, please try again later
sms.code.not.blank=Sms code cannot be blank
sms.code.retry.limit.count=Sms code input error {0} times
sms.code.retry.limit.exceed=Sms code input error {0} times, account locked for {1} minutes
email.code.not.blank=Email code cannot be blank
email.code.retry.limit.count=Email code input error {0} times
email.code.retry.limit.exceed=Email code input error {0} times, account locked for {1} minutes
xcx.code.not.blank=Mini program [code] cannot be blank
social.source.not.blank=Social login platform [source] cannot be blank
social.code.not.blank=Social login platform [code] cannot be blank
social.state.not.blank=Social login platform [state] cannot be blank
##租户
tenant.number.not.blank=Tenant number cannot be blank
tenant.not.exists=Sorry, your tenant does not exist. Please contact the administrator
tenant.blocked=Sorry, your tenant is disabled. Please contact the administrator
tenant.expired=Sorry, your tenant has expired. Please contact the administrator.

View File

@ -0,0 +1,61 @@
#错误消息
not.null=* 必须填写
user.jcaptcha.error=验证码错误
user.jcaptcha.expire=验证码已失效
user.not.exists=对不起, 您的账号:{0} 不存在.
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
user.password.delete=对不起,您的账号:{0} 已被删除
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
role.blocked=角色已封禁,请联系管理员
user.logout.success=退出成功
length.not.valid=长度必须在{min}到{max}个字符之间
user.username.not.blank=用户名不能为空
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成且必须以非数字开头
user.username.length.valid=账户长度必须在{min}到{max}个字符之间
user.password.not.blank=用户密码不能为空
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
user.password.not.valid=* 5-50个字符
user.email.not.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空
user.phonenumber.not.blank=用户手机号不能为空
user.mobile.phone.number.not.valid=手机号格式错误
user.login.success=登录成功
user.register.success=注册成功
user.register.save.error=保存用户 {0} 失败,注册账号已存在
user.register.error=注册失败,请联系系统管理人员
user.notfound=请重新登录
user.forcelogout=管理员强制退出,请重新登录
user.unknown.error=未知错误,请重新登录
auth.grant.type.error=认证权限类型错误
auth.grant.type.blocked=认证权限类型已禁用
auth.grant.type.not.blank=认证权限类型不能为空
auth.clientid.not.blank=认证客户端id不能为空
##文件上传消息
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB
upload.filename.exceed.length=上传的文件名最长{0}个字符
##权限
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
repeat.submit.message=不允许重复提交,请稍候再试
rate.limiter.message=访问过于频繁,请稍候再试
sms.code.not.blank=短信验证码不能为空
sms.code.retry.limit.count=短信验证码输入错误{0}次
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
email.code.not.blank=邮箱验证码不能为空
email.code.retry.limit.count=邮箱验证码输入错误{0}次
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟
xcx.code.not.blank=小程序[code]不能为空
social.source.not.blank=第三方登录平台[source]不能为空
social.code.not.blank=第三方登录平台[code]不能为空
social.state.not.blank=第三方登录平台[state]不能为空
##租户
tenant.number.not.blank=租户编号不能为空
tenant.not.exists=对不起, 您的租户不存在,请联系管理员
tenant.blocked=对不起,您的租户已禁用,请联系管理员
tenant.expired=对不起,您的租户已过期,请联系管理员

Binary file not shown.

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="log.path" value="./logs"/>
<property name="console.log.pattern"
value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${console.log.pattern}</pattern>
<charset>utf-8</charset>
</encoder>
</appender>
<!-- 控制台输出 -->
<appender name="file_console" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-console.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-console.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大 1天 -->
<maxHistory>1</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>utf-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
</filter>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- info异步输出 -->
<appender name="async_info" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="file_info"/>
</appender>
<!-- error异步输出 -->
<appender name="async_error" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="file_error"/>
</appender>
<!-- 整合 skywalking 控制台输出 tid -->
<!-- <appender name="console" class="ch.qos.logback.core.ConsoleAppender">-->
<!-- <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">-->
<!-- <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">-->
<!-- <pattern>[%tid] ${console.log.pattern}</pattern>-->
<!-- </layout>-->
<!-- <charset>utf-8</charset>-->
<!-- </encoder>-->
<!-- </appender>-->
<!-- 整合 skywalking 推送采集日志 -->
<!-- <appender name="sky_log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">-->
<!-- <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">-->
<!-- <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">-->
<!-- <pattern>[%tid] ${console.log.pattern}</pattern>-->
<!-- </layout>-->
<!-- <charset>utf-8</charset>-->
<!-- </encoder>-->
<!-- </appender>-->
<!--系统操作日志-->
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="async_info" />
<appender-ref ref="async_error" />
<appender-ref ref="file_console" />
<!-- <appender-ref ref="sky_log"/>-->
</root>
</configuration>

View File

@ -0,0 +1,45 @@
package org.dromara.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* 断言单元测试案例
*
* @author Lion Li
*/
@DisplayName("断言单元测试案例")
public class AssertUnitTest {
@DisplayName("测试 assertEquals 方法")
@Test
public void testAssertEquals() {
Assertions.assertEquals("666", new String("666"));
Assertions.assertNotEquals("666", new String("666"));
}
@DisplayName("测试 assertSame 方法")
@Test
public void testAssertSame() {
Object obj = new Object();
Object obj1 = obj;
Assertions.assertSame(obj, obj1);
Assertions.assertNotSame(obj, obj1);
}
@DisplayName("测试 assertTrue 方法")
@Test
public void testAssertTrue() {
Assertions.assertTrue(true);
Assertions.assertFalse(true);
}
@DisplayName("测试 assertNull 方法")
@Test
public void testAssertNull() {
Assertions.assertNull(null);
Assertions.assertNotNull(null);
}
}

View File

@ -0,0 +1,70 @@
package org.dromara.test;
import org.dromara.common.web.config.properties.CaptchaProperties;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.TimeUnit;
/**
* 单元测试案例
*
* @author Lion Li
*/
@SpringBootTest // 此注解只能在 springboot 主包下使用 需包含 main 方法与 yml 配置文件
@DisplayName("单元测试案例")
public class DemoUnitTest {
@Autowired
private CaptchaProperties captchaProperties;
@DisplayName("测试 @SpringBootTest @Test @DisplayName 注解")
@Test
public void testTest() {
System.out.println(captchaProperties);
}
@Disabled
@DisplayName("测试 @Disabled 注解")
@Test
public void testDisabled() {
System.out.println(captchaProperties);
}
@Timeout(value = 2L, unit = TimeUnit.SECONDS)
@DisplayName("测试 @Timeout 注解")
@Test
public void testTimeout() throws InterruptedException {
Thread.sleep(3000);
System.out.println(captchaProperties);
}
@DisplayName("测试 @RepeatedTest 注解")
@RepeatedTest(3)
public void testRepeatedTest() {
System.out.println(666);
}
@BeforeAll
public static void testBeforeAll() {
System.out.println("@BeforeAll ==================");
}
@BeforeEach
public void testBeforeEach() {
System.out.println("@BeforeEach ==================");
}
@AfterEach
public void testAfterEach() {
System.out.println("@AfterEach ==================");
}
@AfterAll
public static void testAfterAll() {
System.out.println("@AfterAll ==================");
}
}

View File

@ -0,0 +1,72 @@
package org.dromara.test;
import org.dromara.common.core.enums.UserType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
/**
* 带参数单元测试案例
*
* @author Lion Li
*/
@DisplayName("带参数单元测试案例")
public class ParamUnitTest {
@DisplayName("测试 @ValueSource 注解")
@ParameterizedTest
@ValueSource(strings = {"t1", "t2", "t3"})
public void testValueSource(String str) {
System.out.println(str);
}
@DisplayName("测试 @NullSource 注解")
@ParameterizedTest
@NullSource
public void testNullSource(String str) {
System.out.println(str);
}
@DisplayName("测试 @EnumSource 注解")
@ParameterizedTest
@EnumSource(UserType.class)
public void testEnumSource(UserType type) {
System.out.println(type.getUserType());
}
@DisplayName("测试 @MethodSource 注解")
@ParameterizedTest
@MethodSource("getParam")
public void testMethodSource(String str) {
System.out.println(str);
}
public static Stream<String> getParam() {
List<String> list = new ArrayList<>();
list.add("t1");
list.add("t2");
list.add("t3");
return list.stream();
}
@BeforeEach
public void testBeforeEach() {
System.out.println("@BeforeEach ==================");
}
@AfterEach
public void testAfterEach() {
System.out.println("@AfterEach ==================");
}
}

View File

@ -0,0 +1,54 @@
package org.dromara.test;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.context.SpringBootTest;
/**
* 标签单元测试案例
*
* @author Lion Li
*/
@SpringBootTest
@DisplayName("标签单元测试案例")
public class TagUnitTest {
@Tag("dev")
@DisplayName("测试 @Tag dev")
@Test
public void testTagDev() {
System.out.println("dev");
}
@Tag("prod")
@DisplayName("测试 @Tag prod")
@Test
public void testTagProd() {
System.out.println("prod");
}
@Tag("local")
@DisplayName("测试 @Tag local")
@Test
public void testTagLocal() {
System.out.println("local");
}
@Tag("exclude")
@DisplayName("测试 @Tag exclude")
@Test
public void testTagExclude() {
System.out.println("exclude");
}
@BeforeEach
public void testBeforeEach() {
System.out.println("@BeforeEach ==================");
}
@AfterEach
public void testAfterEach() {
System.out.println("@AfterEach ==================");
}
}

View File

@ -0,0 +1 @@
3f2ee348-0303-40ca-bf03-03f48d2d2141

Binary file not shown.

View File

@ -0,0 +1,4 @@
3
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso10646-1
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso8859-1
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-koi8-r

View File

@ -0,0 +1,4 @@
3
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso10646-1
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso8859-1
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-koi8-r

46
ruoyi-common/pom.xml Normal file
View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi-vue-plus</artifactId>
<groupId>org.dromara</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<modules>
<module>ruoyi-common-bom</module>
<module>ruoyi-common-social</module>
<module>ruoyi-common-core</module>
<module>ruoyi-common-doc</module>
<module>ruoyi-common-excel</module>
<module>ruoyi-common-idempotent</module>
<module>ruoyi-common-job</module>
<module>ruoyi-common-log</module>
<module>ruoyi-common-mail</module>
<module>ruoyi-common-mybatis</module>
<module>ruoyi-common-oss</module>
<module>ruoyi-common-ratelimiter</module>
<module>ruoyi-common-redis</module>
<module>ruoyi-common-satoken</module>
<module>ruoyi-common-security</module>
<module>ruoyi-common-sms</module>
<module>ruoyi-common-web</module>
<module>ruoyi-common-translation</module>
<module>ruoyi-common-sensitive</module>
<module>ruoyi-common-json</module>
<module>ruoyi-common-encrypt</module>
<module>ruoyi-common-tenant</module>
<module>ruoyi-common-websocket</module>
<module>ruoyi-common-sse</module>
</modules>
<artifactId>ruoyi-common</artifactId>
<packaging>pom</packaging>
<description>
common 通用模块
</description>
</project>

View File

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-bom</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<description>
ruoyi-common-bom common依赖项
</description>
<properties>
<revision>5.3.1</revision>
</properties>
<dependencyManagement>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
<version>${revision}</version>
</dependency>
<!-- 接口模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
<version>${revision}</version>
</dependency>
<!-- excel -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-excel</artifactId>
<version>${revision}</version>
</dependency>
<!-- 幂等 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-idempotent</artifactId>
<version>${revision}</version>
</dependency>
<!-- 调度模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-job</artifactId>
<version>${revision}</version>
</dependency>
<!-- 日志记录 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
<version>${revision}</version>
</dependency>
<!-- 邮件服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据库服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
<version>${revision}</version>
</dependency>
<!-- OSS -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-oss</artifactId>
<version>${revision}</version>
</dependency>
<!-- 限流 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ratelimiter</artifactId>
<version>${revision}</version>
</dependency>
<!-- 缓存服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
<version>${revision}</version>
</dependency>
<!-- satoken -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
<version>${revision}</version>
</dependency>
<!-- 安全模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
<version>${revision}</version>
</dependency>
<!-- 短信模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sms</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-social</artifactId>
<version>${revision}</version>
</dependency>
<!-- web服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
<version>${revision}</version>
</dependency>
<!-- 翻译模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-translation</artifactId>
<version>${revision}</version>
</dependency>
<!-- 脱敏模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sensitive</artifactId>
<version>${revision}</version>
</dependency>
<!-- 序列化模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据库加解密模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-encrypt</artifactId>
<version>${revision}</version>
</dependency>
<!-- 租户模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-tenant</artifactId>
<version>${revision}</version>
</dependency>
<!-- WebSocket模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-websocket</artifactId>
<version>${revision}</version>
</dependency>
<!-- SSE模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sse</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-core</artifactId>
<description>
ruoyi-common-core 核心模块
</description>
<dependencies>
<!-- Spring框架基本的核心工具 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- SpringWeb模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- 自定义验证注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 自动生成YML配置关联JSON文件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
</dependency>
<!-- 离线IP地址定位库 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,17 @@
package org.dromara.common.core.config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* 程序注解配置
*
* @author Lion Li
*/
@AutoConfiguration
@EnableAspectJAutoProxy
@EnableAsync(proxyTargetClass = true)
public class ApplicationConfig {
}

View File

@ -0,0 +1,52 @@
package org.dromara.common.core.config;
import cn.hutool.core.util.ArrayUtil;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import java.util.Arrays;
import java.util.concurrent.Executor;
/**
* 异步配置
* <p>
* 如果未使用虚拟线程则生效
*
* @author Lion Li
*/
@AutoConfiguration
public class AsyncConfig implements AsyncConfigurer {
/**
* 自定义 @Async 注解使用系统线程池
*/
@Override
public Executor getAsyncExecutor() {
if(SpringUtils.isVirtual()) {
return new VirtualThreadTaskExecutor("async-");
}
return SpringUtils.getBean("scheduledExecutorService");
}
/**
* 异步执行异常处理
*/
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
throwable.printStackTrace();
StringBuilder sb = new StringBuilder();
sb.append("Exception message - ").append(throwable.getMessage())
.append(", Method name - ").append(method.getName());
if (ArrayUtil.isNotEmpty(objects)) {
sb.append(", Parameter value - ").append(Arrays.toString(objects));
}
throw new ServiceException(sb.toString());
};
}
}

View File

@ -0,0 +1,87 @@
package org.dromara.common.core.config;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.dromara.common.core.config.properties.ThreadPoolProperties;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.Threads;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置
*
* @author Lion Li
**/
@Slf4j
@AutoConfiguration
@EnableConfigurationProperties(ThreadPoolProperties.class)
public class ThreadPoolConfig {
/**
* 核心线程数 = cpu 核心数 + 1
*/
private final int core = Runtime.getRuntime().availableProcessors() + 1;
private ScheduledExecutorService scheduledExecutorService;
@Bean(name = "threadPoolTaskExecutor")
@ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(core);
executor.setMaxPoolSize(core * 2);
executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
/**
* 执行周期性或定时任务
*/
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService() {
// daemon 必须为 true
BasicThreadFactory.Builder builder = new BasicThreadFactory.Builder().daemon(true);
if (SpringUtils.isVirtual()) {
builder.namingPattern("virtual-schedule-pool-%d").wrappedFactory(new VirtualThreadTaskExecutor().getVirtualThreadFactory());
} else {
builder.namingPattern("schedule-pool-%d");
}
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(core,
builder.build(),
new ThreadPoolExecutor.CallerRunsPolicy()) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
Threads.printException(r, t);
}
};
this.scheduledExecutorService = scheduledThreadPoolExecutor;
return scheduledThreadPoolExecutor;
}
/**
* 销毁事件
*/
@PreDestroy
public void destroy() {
try {
log.info("====关闭后台任务任务线程池====");
Threads.shutdownAndAwaitTermination(scheduledExecutorService);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,41 @@
package org.dromara.common.core.config;
import jakarta.validation.Validator;
import org.hibernate.validator.HibernateValidator;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import java.util.Properties;
/**
* 校验框架配置类
*
* @author Lion Li
*/
@AutoConfiguration(before = ValidationAutoConfiguration.class)
public class ValidatorConfig {
/**
* 配置校验框架 快速失败模式
*/
@Bean
public Validator validator(MessageSource messageSource) {
try (LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean()) {
// 国际化
factoryBean.setValidationMessageSource(messageSource);
// 设置使用 HibernateValidator 校验器
factoryBean.setProviderClass(HibernateValidator.class);
Properties properties = new Properties();
// 设置快速失败模式fail-fast即校验过程中一旦遇到失败立即停止并返回错误
properties.setProperty("hibernate.validator.fail_fast", "true");
factoryBean.setValidationProperties(properties);
// 加载配置
factoryBean.afterPropertiesSet();
return factoryBean.getValidator();
}
}
}

View File

@ -0,0 +1,30 @@
package org.dromara.common.core.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 线程池 配置属性
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "thread-pool")
public class ThreadPoolProperties {
/**
* 是否开启线程池
*/
private boolean enabled;
/**
* 队列最大长度
*/
private int queueCapacity;
/**
* 线程池维护线程所允许的空闲时间
*/
private int keepAliveSeconds;
}

View File

@ -0,0 +1,30 @@
package org.dromara.common.core.constant;
/**
* 缓存的key 常量
*
* @author Lion Li
*/
public interface CacheConstants {
/**
* 在线用户 redis key
*/
String ONLINE_TOKEN_KEY = "online_tokens:";
/**
* 参数管理 cache key
*/
String SYS_CONFIG_KEY = "sys_config:";
/**
* 字典管理 cache key
*/
String SYS_DICT_KEY = "sys_dict:";
/**
* 登录账户密码错误次数 redis key
*/
String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
}

View File

@ -0,0 +1,88 @@
package org.dromara.common.core.constant;
/**
* 缓存组名称常量
* <p>
* key 格式为 cacheNames#ttl#maxIdleTime#maxSize
* <p>
* ttl 过期时间 如果设置为0则不过期 默认为0
* maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0
* maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0
* <p>
* 例子: test#60stest#0#60stest#0#1m#1000test#1h#0#500
*
* @author Lion Li
*/
public interface CacheNames {
/**
* 演示案例
*/
String DEMO_CACHE = "demo:cache#60s#10m#20";
/**
* 系统配置
*/
String SYS_CONFIG = "sys_config";
/**
* 数据字典
*/
String SYS_DICT = "sys_dict";
/**
* 数据字典类型
*/
String SYS_DICT_TYPE = "sys_dict_type";
/**
* 租户
*/
String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
/**
* 客户端
*/
String SYS_CLIENT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_client#30d";
/**
* 用户账户
*/
String SYS_USER_NAME = "sys_user_name#30d";
/**
* 用户名称
*/
String SYS_NICKNAME = "sys_nickname#30d";
/**
* 部门
*/
String SYS_DEPT = "sys_dept#30d";
/**
* OSS内容
*/
String SYS_OSS = "sys_oss#30d";
/**
* 角色自定义权限
*/
String SYS_ROLE_CUSTOM = "sys_role_custom#30d";
/**
* 部门及以下权限
*/
String SYS_DEPT_AND_CHILD = "sys_dept_and_child#30d";
/**
* OSS配置
*/
String SYS_OSS_CONFIG = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss_config";
/**
* 在线用户
*/
String ONLINE_TOKEN = "online_tokens";
}

View File

@ -0,0 +1,76 @@
package org.dromara.common.core.constant;
/**
* 通用常量信息
*
* @author ruoyi
*/
public interface Constants {
/**
* UTF-8 字符集
*/
String UTF8 = "UTF-8";
/**
* GBK 字符集
*/
String GBK = "GBK";
/**
* www主域
*/
String WWW = "www.";
/**
* http请求
*/
String HTTP = "http://";
/**
* https请求
*/
String HTTPS = "https://";
/**
* 通用成功标识
*/
String SUCCESS = "0";
/**
* 通用失败标识
*/
String FAIL = "1";
/**
* 登录成功
*/
String LOGIN_SUCCESS = "Success";
/**
* 注销
*/
String LOGOUT = "Logout";
/**
* 注册
*/
String REGISTER = "Register";
/**
* 登录失败
*/
String LOGIN_FAIL = "Error";
/**
* 验证码有效期分钟
*/
Integer CAPTCHA_EXPIRATION = 2;
/**
* 顶级父级id
*/
Long TOP_PARENT_ID = 0L;
}

View File

@ -0,0 +1,34 @@
package org.dromara.common.core.constant;
/**
* 全局的key常量 (业务无关的key)
*
* @author Lion Li
*/
public interface GlobalConstants {
/**
* 全局 redis key (业务无关的key)
*/
String GLOBAL_REDIS_KEY = "global:";
/**
* 验证码 redis key
*/
String CAPTCHA_CODE_KEY = GLOBAL_REDIS_KEY + "captcha_codes:";
/**
* 防重提交 redis key
*/
String REPEAT_SUBMIT_KEY = GLOBAL_REDIS_KEY + "repeat_submit:";
/**
* 限流 redis key
*/
String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";
/**
* 三方认证 redis key
*/
String SOCIAL_AUTH_CODE_KEY = GLOBAL_REDIS_KEY + "social_auth_codes:";
}

View File

@ -0,0 +1,93 @@
package org.dromara.common.core.constant;
/**
* 返回状态码
*
* @author Lion Li
*/
public interface HttpStatus {
/**
* 操作成功
*/
int SUCCESS = 200;
/**
* 对象创建成功
*/
int CREATED = 201;
/**
* 请求已经被接受
*/
int ACCEPTED = 202;
/**
* 操作已经执行成功但是没有返回数据
*/
int NO_CONTENT = 204;
/**
* 资源已被移除
*/
int MOVED_PERM = 301;
/**
* 重定向
*/
int SEE_OTHER = 303;
/**
* 资源没有被修改
*/
int NOT_MODIFIED = 304;
/**
* 参数列表错误缺少格式不匹配
*/
int BAD_REQUEST = 400;
/**
* 未授权
*/
int UNAUTHORIZED = 401;
/**
* 访问受限授权过期
*/
int FORBIDDEN = 403;
/**
* 资源服务未找到
*/
int NOT_FOUND = 404;
/**
* 不允许的http方法
*/
int BAD_METHOD = 405;
/**
* 资源冲突或者资源被锁
*/
int CONFLICT = 409;
/**
* 不支持的数据媒体类型
*/
int UNSUPPORTED_TYPE = 415;
/**
* 系统内部错误
*/
int ERROR = 500;
/**
* 接口未实现
*/
int NOT_IMPLEMENTED = 501;
/**
* 系统警告消息
*/
int WARN = 601;
}

View File

@ -0,0 +1,59 @@
package org.dromara.common.core.constant;
import cn.hutool.core.lang.RegexPool;
/**
* 常用正则表达式字符串
* <p>
* 常用正则表达式集合更多正则见: https://any86.github.io/any-rule/
*
* @author Feng
*/
public interface RegexConstants extends RegexPool {
/**
* 字典类型必须以字母开头且只能为小写字母数字下滑线
*/
String DICTIONARY_TYPE = "^[a-z][a-z0-9_]*$";
/**
* 权限标识必须符合以下格式
* 1. 标准格式xxx:yyy:zzz
* - 第一部分xxx只能包含字母数字和下划线_不能使用 `*`
* - 第二部分yyy可以包含字母数字下划线_ `*`
* - 第三部分zzz可以包含字母数字下划线_ `*`
* 2. 允许空字符串""表示没有权限标识
*/
String PERMISSION_STRING = "^$|^[a-zA-Z0-9_]+:[a-zA-Z0-9_*]+:[a-zA-Z0-9_*]+$";
/**
* 身份证号码后6位
*/
String ID_CARD_LAST_6 = "^(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
/**
* QQ号码
*/
String QQ_NUMBER = "^[1-9][0-9]\\d{4,9}$";
/**
* 邮政编码
*/
String POSTAL_CODE = "^[1-9]\\d{5}$";
/**
* 注册账号
*/
String ACCOUNT = "^[a-zA-Z][a-zA-Z0-9_]{4,15}$";
/**
* 密码包含至少8个字符包括大写字母小写字母数字和特殊字符
*/
String PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
/**
* 通用状态0表示正常1表示停用
*/
String STATUS = "^[01]$";
}

View File

@ -0,0 +1,80 @@
package org.dromara.common.core.constant;
/**
* 系统常量信息
*
* @author Lion Li
*/
public interface SystemConstants {
/**
* 正常状态
*/
String NORMAL = "0";
/**
* 异常状态
*/
String DISABLE = "1";
/**
* 是否为系统默认
*/
String YES = "Y";
/**
* 是否为系统默认
*/
String NO = "N";
/**
* 是否菜单外链
*/
String YES_FRAME = "0";
/**
* 是否菜单外链
*/
String NO_FRAME = "1";
/**
* 菜单类型目录
*/
String TYPE_DIR = "M";
/**
* 菜单类型菜单
*/
String TYPE_MENU = "C";
/**
* 菜单类型按钮
*/
String TYPE_BUTTON = "F";
/**
* Layout组件标识
*/
String LAYOUT = "Layout";
/**
* ParentView组件标识
*/
String PARENT_VIEW = "ParentView";
/**
* InnerLink组件标识
*/
String INNER_LINK = "InnerLink";
/**
* 超级管理员ID
*/
Long SUPER_ADMIN_ID = 1L;
/**
* 根部门祖级列表
*/
String ROOT_DEPT_ANCESTORS = "0";
}

View File

@ -0,0 +1,35 @@
package org.dromara.common.core.constant;
/**
* 租户常量信息
*
* @author Lion Li
*/
public interface TenantConstants {
/**
* 超级管理员ID
*/
Long SUPER_ADMIN_ID = 1L;
/**
* 超级管理员角色 roleKey
*/
String SUPER_ADMIN_ROLE_KEY = "superadmin";
/**
* 租户管理员角色 roleKey
*/
String TENANT_ADMIN_ROLE_KEY = "admin";
/**
* 租户管理员角色名称
*/
String TENANT_ADMIN_ROLE_NAME = "管理员";
/**
* 默认租户ID
*/
String DEFAULT_TENANT_ID = "000000";
}

View File

@ -0,0 +1,110 @@
package org.dromara.common.core.domain;
import org.dromara.common.core.constant.HttpStatus;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 响应信息主体
*
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class R<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 成功
*/
public static final int SUCCESS = 200;
/**
* 失败
*/
public static final int FAIL = 500;
private int code;
private String msg;
private T data;
public static <T> R<T> ok() {
return restResult(null, SUCCESS, "操作成功");
}
public static <T> R<T> ok(T data) {
return restResult(data, SUCCESS, "操作成功");
}
public static <T> R<T> ok(String msg) {
return restResult(null, SUCCESS, msg);
}
public static <T> R<T> ok(String msg, T data) {
return restResult(data, SUCCESS, msg);
}
public static <T> R<T> fail() {
return restResult(null, FAIL, "操作失败");
}
public static <T> R<T> fail(String msg) {
return restResult(null, FAIL, msg);
}
public static <T> R<T> fail(T data) {
return restResult(data, FAIL, "操作失败");
}
public static <T> R<T> fail(String msg, T data) {
return restResult(data, FAIL, msg);
}
public static <T> R<T> fail(int code, String msg) {
return restResult(null, code, msg);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static <T> R<T> warn(String msg) {
return restResult(null, HttpStatus.WARN, msg);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static <T> R<T> warn(String msg, T data) {
return restResult(data, HttpStatus.WARN, msg);
}
private static <T> R<T> restResult(T data, int code, String msg) {
R<T> r = new R<>();
r.setCode(code);
r.setData(data);
r.setMsg(msg);
return r;
}
public static <T> Boolean isError(R<T> ret) {
return !isSuccess(ret);
}
public static <T> Boolean isSuccess(R<T> ret) {
return R.SUCCESS == ret.getCode();
}
}

View File

@ -0,0 +1,71 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 办理任务请求对象
*
* @author may
*/
@Data
public class CompleteTaskDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 任务id
*/
private Long taskId;
/**
* 附件id
*/
private String fileId;
/**
* 抄送人员
*/
private List<FlowCopyDTO> flowCopyList;
/**
* 消息类型
*/
private List<String> messageType;
/**
* 办理意见
*/
private String message;
/**
* 消息通知
*/
private String notice;
/**
* 流程变量
*/
private Map<String, Object> variables;
/**
* 扩展变量(此处为逗号分隔的ossId)
*/
private String ext;
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
}

View File

@ -0,0 +1,36 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 部门
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class DeptDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 部门ID
*/
private Long deptId;
/**
* 父部门ID
*/
private Long parentId;
/**
* 部门名称
*/
private String deptName;
}

View File

@ -0,0 +1,41 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 字典数据DTO
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class DictDataDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 字典标签
*/
private String dictLabel;
/**
* 字典键值
*/
private String dictValue;
/**
* 是否默认Y是 N否
*/
private String isDefault;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,41 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 字典类型DTO
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class DictTypeDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 字典主键
*/
private Long dictId;
/**
* 字典名称
*/
private String dictName;
/**
* 字典类型
*/
private String dictType;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,30 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 抄送
*
* @author may
*/
@Data
public class FlowCopyDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
private Long userId;
/**
* 用户名称
*/
private String userName;
}

View File

@ -0,0 +1,46 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* OSS对象
*
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class OssDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 对象存储主键
*/
private Long ossId;
/**
* 文件名
*/
private String fileName;
/**
* 原名
*/
private String originalName;
/**
* 文件后缀名
*/
private String fileSuffix;
/**
* URL地址
*/
private String url;
}

View File

@ -0,0 +1,46 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 岗位
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class PostDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 岗位ID
*/
private Long postId;
/**
* 部门id
*/
private Long deptId;
/**
* 岗位编码
*/
private String postCode;
/**
* 岗位名称
*/
private String postName;
/**
* 岗位类别编码
*/
private String postCategory;
}

View File

@ -0,0 +1,42 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 角色
*
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class RoleDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 角色ID
*/
private Long roleId;
/**
* 角色名称
*/
private String roleName;
/**
* 角色权限
*/
private String roleKey;
/**
* 数据范围1全部数据权限 2自定数据权限 3本部门数据权限 4本部门及以下数据权限 5仅本人数据权限 6部门及以下或本人数据权限
*/
private String dataScope;
}

View File

@ -0,0 +1,45 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 启动流程对象
*
* @author may
*/
@Data
public class StartProcessDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 业务唯一值id
*/
private String businessId;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 流程变量前端会提交一个元素{'entity': {业务详情数据对象}}
*/
private Map<String, Object> variables;
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
}

View File

@ -0,0 +1,30 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 启动流程返回对象
*
* @author Lion Li
*/
@Data
public class StartProcessReturnDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程实例id
*/
private Long processInstanceId;
/**
* 任务id
*/
private Long taskId;
}

View File

@ -0,0 +1,101 @@
package org.dromara.common.core.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 任务受让人
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class TaskAssigneeDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 总大小
*/
private Long total = 0L;
/**
*
*/
private List<TaskHandler> list;
public TaskAssigneeDTO(Long total, List<TaskHandler> list) {
this.total = total;
this.list = list;
}
/**
* 将源列表转换为 TaskHandler 列表
*
* @param <T> 通用类型
* @param sourceList 待转换的源列表
* @param storageId 提取 storageId 的函数
* @param handlerCode 提取 handlerCode 的函数
* @param handlerName 提取 handlerName 的函数
* @param groupName 提取 groupName 的函数
* @param createTimeMapper 提取 createTime 的函数
* @return 转换后的 TaskHandler 列表
*/
public static <T> List<TaskHandler> convertToHandlerList(
List<T> sourceList,
Function<T, Long> storageId,
Function<T, String> handlerCode,
Function<T, String> handlerName,
Function<T, Long> groupName,
Function<T, Date> createTimeMapper) {
return sourceList.stream()
.map(item -> new TaskHandler(
String.valueOf(storageId.apply(item)),
handlerCode.apply(item),
handlerName.apply(item),
groupName != null ? String.valueOf(groupName.apply(item)) : null,
createTimeMapper.apply(item)
)).collect(Collectors.toList());
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class TaskHandler {
/**
* 主键
*/
private String storageId;
/**
* 权限编码
*/
private String handlerCode;
/**
* 权限名称
*/
private String handlerName;
/**
* 权限分组
*/
private String groupName;
/**
* 创建时间
*/
private Date createTime;
}
}

View File

@ -0,0 +1,73 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 用户
*
* @author Michelle.Chung
*/
@Data
@NoArgsConstructor
public class UserDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 部门ID
*/
private Long deptId;
/**
* 用户账号
*/
private String userName;
/**
* 用户昵称
*/
private String nickName;
/**
* 用户类型sys_user系统用户
*/
private String userType;
/**
* 用户邮箱
*/
private String email;
/**
* 手机号码
*/
private String phonenumber;
/**
* 用户性别0男 1女 2未知
*/
private String sex;
/**
* 帐号状态0正常 1停用
*/
private String status;
/**
* 创建时间
*/
private Date createTime;
}

View File

@ -0,0 +1,72 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 当前在线会话
*
* @author ruoyi
*/
@Data
@NoArgsConstructor
public class UserOnlineDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 会话编号
*/
private String tokenId;
/**
* 部门名称
*/
private String deptName;
/**
* 用户名称
*/
private String userName;
/**
* 客户端
*/
private String clientKey;
/**
* 设备类型
*/
private String deviceType;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地址
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 登录时间
*/
private Long loginTime;
}

View File

@ -0,0 +1,44 @@
package org.dromara.common.core.domain.event;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 流程创建任务监听
*
* @author may
*/
@Data
public class ProcessCreateTaskEvent implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 审批节点编码
*/
private String nodeCode;
/**
* 任务id
*/
private Long taskId;
/**
* 业务id
*/
private String businessId;
}

View File

@ -0,0 +1,34 @@
package org.dromara.common.core.domain.event;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 删除流程监听
*
* @author AprilWind
*/
@Data
public class ProcessDeleteEvent implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 业务id
*/
private String businessId;
}

View File

@ -0,0 +1,50 @@
package org.dromara.common.core.domain.event;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Map;
/**
* 总体流程监听
*
* @author may
*/
@Data
public class ProcessEvent implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 业务id
*/
private String businessId;
/**
* 状态
*/
private String status;
/**
* 办理参数
*/
private Map<String, Object> params;
/**
* 当为true时为申请人节点办理
*/
private boolean submit;
}

View File

@ -0,0 +1,31 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 邮件登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class EmailLoginBody extends LoginBody {
/**
* 邮箱
*/
@NotBlank(message = "{user.email.not.blank}")
@Email(message = "{user.email.not.valid}")
private String email;
/**
* 邮箱code
*/
@NotBlank(message = "{email.code.not.blank}")
private String emailCode;
}

View File

@ -0,0 +1,48 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 用户登录对象
*
* @author Lion Li
*/
@Data
public class LoginBody implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 客户端id
*/
@NotBlank(message = "{auth.clientid.not.blank}")
private String clientId;
/**
* 授权类型
*/
@NotBlank(message = "{auth.grant.type.not.blank}")
private String grantType;
/**
* 租户ID
*/
private String tenantId;
/**
* 验证码
*/
private String code;
/**
* 唯一标识
*/
private String uuid;
}

View File

@ -0,0 +1,148 @@
package org.dromara.common.core.domain.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.dromara.common.core.domain.dto.PostDTO;
import org.dromara.common.core.domain.dto.RoleDTO;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
/**
* 登录用户身份权限
*
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class LoginUser implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/**
* 用户ID
*/
private Long userId;
/**
* 部门ID
*/
private Long deptId;
/**
* 部门类别编码
*/
private String deptCategory;
/**
* 部门名
*/
private String deptName;
/**
* 用户唯一标识
*/
private String token;
/**
* 用户类型
*/
private String userType;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 菜单权限
*/
private Set<String> menuPermission;
/**
* 角色权限
*/
private Set<String> rolePermission;
/**
* 用户名
*/
private String username;
/**
* 用户昵称
*/
private String nickname;
/**
* 角色对象
*/
private List<RoleDTO> roles;
/**
* 岗位对象
*/
private List<PostDTO> posts;
/**
* 数据权限 当前角色ID
*/
private Long roleId;
/**
* 客户端
*/
private String clientKey;
/**
* 设备类型
*/
private String deviceType;
/**
* 获取登录id
*/
public String getLoginId() {
if (userType == null) {
throw new IllegalArgumentException("用户类型不能为空");
}
if (userId == null) {
throw new IllegalArgumentException("用户ID不能为空");
}
return userType + ":" + userId;
}
}

View File

@ -0,0 +1,31 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Length;
/**
* 密码登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PasswordLoginBody extends LoginBody {
/**
* 用户名
*/
@NotBlank(message = "{user.username.not.blank}")
@Length(min = 2, max = 30, message = "{user.username.length.valid}")
private String username;
/**
* 用户密码
*/
@NotBlank(message = "{user.password.not.blank}")
@Length(min = 5, max = 30, message = "{user.password.length.valid}")
private String password;
}

View File

@ -0,0 +1,33 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Length;
/**
* 用户注册对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RegisterBody extends LoginBody {
/**
* 用户名
*/
@NotBlank(message = "{user.username.not.blank}")
@Length(min = 2, max = 20, message = "{user.username.length.valid}")
private String username;
/**
* 用户密码
*/
@NotBlank(message = "{user.password.not.blank}")
@Length(min = 5, max = 20, message = "{user.password.length.valid}")
private String password;
private String userType;
}

View File

@ -0,0 +1,29 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 短信登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SmsLoginBody extends LoginBody {
/**
* 手机号
*/
@NotBlank(message = "{user.phonenumber.not.blank}")
private String phonenumber;
/**
* 短信code
*/
@NotBlank(message = "{sms.code.not.blank}")
private String smsCode;
}

View File

@ -0,0 +1,35 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 三方登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SocialLoginBody extends LoginBody {
/**
* 第三方登录平台
*/
@NotBlank(message = "{social.source.not.blank}")
private String source;
/**
* 第三方登录code
*/
@NotBlank(message = "{social.code.not.blank}")
private String socialCode;
/**
* 第三方登录socialState
*/
@NotBlank(message = "{social.state.not.blank}")
private String socialState;
}

View File

@ -0,0 +1,56 @@
package org.dromara.common.core.domain.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 任务受让人
*
* @author AprilWind
*/
@Data
@NoArgsConstructor
public class TaskAssigneeBody implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 权限编码
*/
private String handlerCode;
/**
* 权限名称
*/
private String handlerName;
/**
* 权限分组
*/
private String groupId;
/**
* 开始时间
*/
private String beginTime;
/**
* 结束时间
*/
private String endTime;
/**
* 当前页
*/
private Integer pageNum = 1;
/**
* 每页显示条数
*/
private Integer pageSize = 10;
}

View File

@ -0,0 +1,28 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 三方登录对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class XcxLoginBody extends LoginBody {
/**
* 小程序id(多个小程序时使用)
*/
private String appid;
/**
* 小程序code
*/
@NotBlank(message = "{xcx.code.not.blank}")
private String xcxCode;
}

View File

@ -0,0 +1,27 @@
package org.dromara.common.core.domain.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serial;
/**
* 小程序登录用户身份权限
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class XcxLoginUser extends LoginUser {
@Serial
private static final long serialVersionUID = 1L;
/**
* openid
*/
private String openid;
}

View File

@ -0,0 +1,215 @@
package org.dromara.common.core.enums;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 业务状态枚举
*
* @author may
*/
@Getter
@AllArgsConstructor
public enum BusinessStatusEnum {
/**
* 已撤销
*/
CANCEL("cancel", "已撤销"),
/**
* 草稿
*/
DRAFT("draft", "草稿"),
/**
* 待审核
*/
WAITING("waiting", "待审核"),
/**
* 已完成
*/
FINISH("finish", "已完成"),
/**
* 已作废
*/
INVALID("invalid", "已作废"),
/**
* 已退回
*/
BACK("back", "已退回"),
/**
* 已终止
*/
TERMINATION("termination", "已终止");
/**
* 状态
*/
private final String status;
/**
* 描述
*/
private final String desc;
private static final Map<String, BusinessStatusEnum> STATUS_MAP = Arrays.stream(BusinessStatusEnum.values())
.collect(Collectors.toConcurrentMap(BusinessStatusEnum::getStatus, Function.identity()));
/**
* 根据状态获取对应的 BusinessStatusEnum 枚举
*
* @param status 业务状态码
* @return 对应的 BusinessStatusEnum 枚举如果找不到则返回 null
*/
public static BusinessStatusEnum getByStatus(String status) {
// 使用 STATUS_MAP 获取对应的枚举若找不到则返回 null
return STATUS_MAP.get(status);
}
/**
* 根据状态获取对应的业务状态描述信息
*
* @param status 业务状态码
* @return 返回业务状态描述若状态码为空或未找到对应的枚举返回空字符串
*/
public static String findByStatus(String status) {
if (StringUtils.isBlank(status)) {
return StrUtil.EMPTY;
}
BusinessStatusEnum statusEnum = STATUS_MAP.get(status);
return (statusEnum != null) ? statusEnum.getDesc() : StrUtil.EMPTY;
}
/**
* 判断是否为指定的状态之一草稿已撤销或已退回
*
* @param status 要检查的状态
* @return 如果状态为草稿已撤销或已退回之一则返回 true否则返回 false
*/
public static boolean isDraftOrCancelOrBack(String status) {
return DRAFT.status.equals(status) || CANCEL.status.equals(status) || BACK.status.equals(status);
}
/**
* 判断是否为撤销退回作废终止
*
* @param status status
* @return 结果
*/
public static boolean initialState(String status) {
return CANCEL.status.equals(status) || BACK.status.equals(status) || INVALID.status.equals(status) || TERMINATION.status.equals(status);
}
/**
* 获取运行中的实例状态列表
*
* @return 包含运行中实例状态的不可变列表
* 包含 DRAFTWAITINGBACK CANCEL 状态
*/
public static List<String> runningStatus() {
return Arrays.asList(DRAFT.status, WAITING.status, BACK.status, CANCEL.status);
}
/**
* 获取结束实例的状态列表
*
* @return 包含结束实例状态的不可变列表
* 包含 FINISHINVALID TERMINATION 状态
*/
public static List<String> finishStatus() {
return Arrays.asList(FINISH.status, INVALID.status, TERMINATION.status);
}
/**
* 启动流程校验
*
* @param status 状态
*/
public static void checkStartStatus(String status) {
if (WAITING.getStatus().equals(status)) {
throw new ServiceException("该单据已提交过申请,正在审批中!");
} else if (FINISH.getStatus().equals(status)) {
throw new ServiceException("该单据已完成申请!");
} else if (INVALID.getStatus().equals(status)) {
throw new ServiceException("该单据已作废!");
} else if (TERMINATION.getStatus().equals(status)) {
throw new ServiceException("该单据已终止!");
} else if (StringUtils.isBlank(status)) {
throw new ServiceException("流程状态为空!");
}
}
/**
* 撤销流程校验
*
* @param status 状态
*/
public static void checkCancelStatus(String status) {
if (CANCEL.getStatus().equals(status)) {
throw new ServiceException("该单据已撤销!");
} else if (FINISH.getStatus().equals(status)) {
throw new ServiceException("该单据已完成申请!");
} else if (INVALID.getStatus().equals(status)) {
throw new ServiceException("该单据已作废!");
} else if (TERMINATION.getStatus().equals(status)) {
throw new ServiceException("该单据已终止!");
} else if (BACK.getStatus().equals(status)) {
throw new ServiceException("该单据已退回!");
} else if (StringUtils.isBlank(status)) {
throw new ServiceException("流程状态为空!");
}
}
/**
* 驳回流程校验
*
* @param status 状态
*/
public static void checkBackStatus(String status) {
if (BACK.getStatus().equals(status)) {
throw new ServiceException("该单据已退回!");
} else if (FINISH.getStatus().equals(status)) {
throw new ServiceException("该单据已完成申请!");
} else if (INVALID.getStatus().equals(status)) {
throw new ServiceException("该单据已作废!");
} else if (TERMINATION.getStatus().equals(status)) {
throw new ServiceException("该单据已终止!");
} else if (CANCEL.getStatus().equals(status)) {
throw new ServiceException("该单据已撤销!");
} else if (StringUtils.isBlank(status)) {
throw new ServiceException("流程状态为空!");
}
}
/**
* 作废,终止流程校验
*
* @param status 状态
*/
public static void checkInvalidStatus(String status) {
if (FINISH.getStatus().equals(status)) {
throw new ServiceException("该单据已完成申请!");
} else if (INVALID.getStatus().equals(status)) {
throw new ServiceException("该单据已作废!");
} else if (TERMINATION.getStatus().equals(status)) {
throw new ServiceException("该单据已终止!");
} else if (StringUtils.isBlank(status)) {
throw new ServiceException("流程状态为空!");
}
}
}

View File

@ -0,0 +1,37 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 设备类型
* 针对一套 用户体系
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum DeviceType {
/**
* pc端
*/
PC("pc"),
/**
* app端
*/
APP("app"),
/**
* 小程序端
*/
XCX("xcx"),
/**
* social第三方端
*/
SOCIAL("social");
private final String device;
}

View File

@ -0,0 +1,146 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.core.utils.StringUtils;
/*
* 日期格式
* "yyyy"4位数的年份例如2023年表示为"2023"
* "yy"2位数的年份例如2023年表示为"23"
* "MM"2位数的月份取值范围为01到12例如7月表示为"07"
* "M"不带前导零的月份取值范围为1到12例如7月表示为"7"
* "dd"2位数的日期取值范围为01到31例如22日表示为"22"
* "d"不带前导零的日期取值范围为1到31例如22日表示为"22"
* "EEEE"星期的全名例如星期三表示为"Wednesday"
* "E"星期的缩写例如星期三表示为"Wed"
* "DDD" "D"一年中的第几天取值范围为001到366例如第200天表示为"200"
* 时间格式
* "HH"24小时制的小时数取值范围为00到23例如下午5点表示为"17"
* "hh"12小时制的小时数取值范围为01到12例如下午5点表示为"05"
* "mm"分钟数取值范围为00到59例如30分钟表示为"30"
* "ss"秒数取值范围为00到59例如45秒表示为"45"
* "SSS"毫秒数取值范围为000到999例如123毫秒表示为"123"
*/
/**
* 日期格式与时间格式枚举
*/
@Getter
@AllArgsConstructor
public enum FormatsType {
/**
* 例如2023年表示为"23"
*/
YY("yy"),
/**
* 例如2023年表示为"2023"
*/
YYYY("yyyy"),
/**
* 例例如2023年7月可以表示为 "2023-07"
*/
YYYY_MM("yyyy-MM"),
/**
* 例如日期 "2023年7月22日" 可以表示为 "2023-07-22"
*/
YYYY_MM_DD("yyyy-MM-dd"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分"则可以表示为 "2023-07-22 15:30"
*/
YYYY_MM_DD_HH_MM("yyyy-MM-dd HH:mm"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分45秒"则可以表示为 "2023-07-22 15:30:45"
*/
YYYY_MM_DD_HH_MM_SS("yyyy-MM-dd HH:mm:ss"),
/**
* 例如下午3点30分45秒表示为 "15:30:45"
*/
HH_MM_SS("HH:mm:ss"),
/**
* 例例如2023年7月可以表示为 "2023/07"
*/
YYYY_MM_SLASH("yyyy/MM"),
/**
* 例如日期 "2023年7月22日" 可以表示为 "2023/07/22"
*/
YYYY_MM_DD_SLASH("yyyy/MM/dd"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分45秒"则可以表示为 "2023/07/22 15:30:45"
*/
YYYY_MM_DD_HH_MM_SLASH("yyyy/MM/dd HH:mm"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分45秒"则可以表示为 "2023/07/22 15:30:45"
*/
YYYY_MM_DD_HH_MM_SS_SLASH("yyyy/MM/dd HH:mm:ss"),
/**
* 例例如2023年7月可以表示为 "2023.07"
*/
YYYY_MM_DOT("yyyy.MM"),
/**
* 例如日期 "2023年7月22日" 可以表示为 "2023.07.22"
*/
YYYY_MM_DD_DOT("yyyy.MM.dd"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分"则可以表示为 "2023.07.22 15:30"
*/
YYYY_MM_DD_HH_MM_DOT("yyyy.MM.dd HH:mm"),
/**
* 例如当前时间如果是 "2023年7月22日下午3点30分45秒"则可以表示为 "2023.07.22 15:30:45"
*/
YYYY_MM_DD_HH_MM_SS_DOT("yyyy.MM.dd HH:mm:ss"),
/**
* 例如2023年7月可以表示为 "202307"
*/
YYYYMM("yyyyMM"),
/**
* 例如2023年7月22日可以表示为 "20230722"
*/
YYYYMMDD("yyyyMMdd"),
/**
* 例如2023年7月22日下午3点可以表示为 "2023072215"
*/
YYYYMMDDHH("yyyyMMddHH"),
/**
* 例如2023年7月22日下午3点30分可以表示为 "202307221530"
*/
YYYYMMDDHHMM("yyyyMMddHHmm"),
/**
* 例如2023年7月22日下午3点30分45秒可以表示为 "20230722153045"
*/
YYYYMMDDHHMMSS("yyyyMMddHHmmss");
/**
* 时间格式
*/
private final String timeFormat;
public static FormatsType getFormatsType(String str) {
for (FormatsType value : values()) {
if (StringUtils.contains(str, value.getTimeFormat())) {
return value;
}
}
throw new RuntimeException("'FormatsType' not found By " + str);
}
}

View File

@ -0,0 +1,44 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 登录类型
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum LoginType {
/**
* 密码登录
*/
PASSWORD("user.password.retry.limit.exceed", "user.password.retry.limit.count"),
/**
* 短信登录
*/
SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
/**
* 邮箱登录
*/
EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"),
/**
* 小程序登录
*/
XCX("", "");
/**
* 登录重试超出限制提示
*/
final String retryLimitExceed;
/**
* 登录重试限制计数提示
*/
final String retryLimitCount;
}

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