docs: 📚 更新文档

This commit is contained in:
bunny 2025-04-29 00:00:37 +08:00
parent 603cdf3285
commit ba36f0966b
20 changed files with 222 additions and 769 deletions

951
ReadMe.md
View File

@ -1,14 +1,14 @@
# 感谢
# BunnyAuth动态权限控制简介
> [!important]
> [!IMPORTANT]
>
> 前端项目整体都由此模板开发
>
> 项目由[小铭](https://github.com/xiaoxian521)开源权限模板[Pure-admin](https://pure-admin.github.io/vue-pure-admin/)
>
> **Pure-admin文档**https://pure-admin.github.io/pure-admin-doc
项目由[小铭](https://github.com/xiaoxian521)开源权限模板[Pure-admin](https://pure-admin.github.io/vue-pure-admin/)
**Pure-admin文档**https://pure-admin.github.io/pure-admin-doc
## 视频和地址
## 视频说明地址
**介绍视频视频**
@ -32,779 +32,214 @@
- 代码生成器前端https://gitee.com/BunnyBoss/generator-code-web
- 代码生成器后端https://gitee.com/BunnyBoss/generator-code-server
![DAOCHU](./images/DAOCHU.png)
## 🚀 项目简介
# 项目预览
一个基于 Spring Security 6 的现代化动态权限控制系统,提供完整的 RBAC 权限管理解决方案。支持前后端分离架构,可灵活配置细粒度权限控制。
**线上地址**
## ✨ v4.0.0 重大更新
正式线上预览地址http://bunny-web.site/#/welcome
### 核心改进
> 如果发现网站打开白屏很有可能是因为更新造成的,因为需要维护删除不需要的功能可能会导致这个情况,但基本不会遇到。
>
> 谷歌方式如下:
>
> ![image-20250108225817891](http://129.211.31.58:9000/docs/image-20250108225817891.png)
>
> Edge方式如下
>
> ![image-20250108225915189](http://129.211.31.58:9000/docs/image-20250108225915189.png)
- **全面重构**后端接口、实体类等重构前端重构部分j+优化操作体验
- **批量操作支持**
- ✅菜单管理:完善属性内容
- ✅ 权限管理:支持 JSON/Excel 导入导出
- ✅ 角色管理:支持 Excel 批量更新
- ✅ 多语言配置:支持 JSON/Excel 更新(全量替换模式)
**打包视频**
### 技术亮点
https://www.bilibili.com/video/BV1AYm8YSEKY/
- **注解扫描**:通过 `AnnotationScanner.java` 自动扫描想要的注解
**Github地址**
```java
// 示例:扫描特定注解的类
public static Set<Class<?>> getClassesWithAnnotation(Class<?> annotation) {
// 实现细节...
}
```
- [前端地址](https://github.com/BunnyMaster/bunny-admin-web.git)
- [后端地址](https://github.com/BunnyMaster/bunny-admin-server)
- **应用场景**
- 定时任务配置
- 权限接口发现
**`Gitee`地址**
### 界面优化
- [前端地址](https://gitee.com/BunnyBoss/bunny-admin-web)
- [后端地址](https://gitee.com/BunnyBoss/bunny-admin-server)
![权限管理界面](./images/image-20250428223816172.png)
![角色管理界面](./images/image-20250428223843974.png)
## 环境搭建
## 🔐 权限控制体系
### 安装docker内容
![image-20250428225337843](./images/image-20250428225337843-1745854181492-5.png)
#### centos或rocky
### 访问规则配置
```shell
# 更新yum 和 dnf
yum update -y
dnf update -y
通过 `WebSecurityConfig` 配置
# 安装必要依赖
yum install -y yum-utils device-mapper-persistent-data lvm2
| 路径类型 | 示例 | 访问要求 | 配置方式 |
| -------- | ----------------- | -------- | ------------------------- |
| 公开接口 | `/api/public/**` | 无需认证 | 路径包含 `public` 关键字 |
| 私有接口 | `/api/private/**` | 需登录 | 路径包含 `private` 关键字 |
# 设置镜像源
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum list docker-ce --showduplicates | sort -r
### 路径匹配策略
# 安装docker
yum -y install docker-ce.x86_64
```java
public static String[] annotations = { ... };
# 开机启动docker
systemctl enable docker
systemctl start docker
// 配置示例
http.authorizeHttpRequests(auth -> auth
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(annotations).permitAll()
);
```
#### Ubuntu环境搭建
```bash
sudo apt-get remove docker docker-engine docker.io containerd runc
sudo apt update
sudo apt upgrade
sudo apt-get install ca-certificates curl gnupg lsb-release
# 添加Docker官方GPG密钥
sudo curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
# 安装docker
sudo apt-get install docker-ce docker-ce-cli containerd.io
# 默认情况下只有root用户和docker组的用户才能运行Docker命令。我们可以将当前用户添加到docker组以避免每次使用Docker时都需要使用sudo设置完成后退出当前用户之后再进入既可
sudo usermod -aG docker $USER
# 运行docker
sudo systemctl start docker
# 安装工具
sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common
# 重启docker
sudo service docker restart
```
避免每次操作docker需要输入sudo
```bash
# 创建分组一般
sudo groupadd docker
# 将当前用户添加到分组
sudo usermod -aG docker $USER
# 重启终端生效
exit
```
> [!warning]
>
> 安装docker会可能会遇到镜像无法拉去问题试下面的几个镜像地址根据你自己的需要进行配置不是所有人都需要同时也不能保证地址真的有效
>
> 复制下面的内容到`daemon.json`
>
> ```json
> # 创建目录
> sudo mkdir -p /etc/docker
>
> # 写入配置文件
> sudo tee /etc/docker/daemon.json <<-'EOF'
> {
> "registry-mirrors": [
> "https://docker-0.unsee.tech",
> "https://docker-cf.registry.cyou",
> "https://docker.1panel.live"
> ]
> }
> EOF
>
> # 重启docker服务
> sudo systemctl daemon-reload && sudo systemctl restart docker
> ```
>
> 重启docker
>
> ```bash
> sudo systemctl restart docker.socket
> ```
## Docker-Compose 搭建环境
### 安装Docker-Compose
使用docker-compose有的时候会因为版本不同但是配置文件主要内容就是这些。需要注意版本问题
#### Centos、Rocky
```bash
sudo yum install docker-ce docker-ce-cli containerd.io
sudo yum install curl
curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.4/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo docker-compose --version
```
#### Ubuntu
```bash
sudo apt install docker-compose
```
### docker-compose文件位置
![image-20250222184020443](http://129.211.31.58:9000/docs/image-20250222184020443.png)
### 相关命令
#### 只拉取镜像
docker-compose pull 命令会拉取 docker-compose.yml 文件中定义的所有服务的镜像,但不会创建或启动容器。
```bash
docker-compose pull
```
#### 拉取镜像而不做其他操作
只拉取镜像而不做其他操作,可以结合 docker-compose 的其他命令来实现:
```bash
docker-compose up --no-start
```
#### 拉取镜像并创建容器
## 🛠️ 应用场景
### 1. 纯前端控制模式
前端原理详情查看Pure文档https://pure-admin.cn/pages/RBAC/#%E5%A6%82%E4%BD%95%E9%85%8D%E7%BD%AE
![image-20250428230444403](./images/image-20250428230444403-1745854157395-3.png)
- **页面控制**
1. 为路由菜单分配角色
2. 为用户分配角色
- **按钮控制**
```ts
// 前端权限码配置
const auth = {
add: ['i18nType::add'],
update: ['i18nType::update'],
delete: ['i18nType::delete'],
};
```
### 2. 纯后端控制模式
- 接口级权限:分页这种就可以添加为`/api/permission/*/*`
```java
@Tag(name = "系统权限")
@RestController
@RequestMapping("api/permission")
public class PermissionController {
@Operation(summary = "分页查询", tags = {"permission::page"})
@GetMapping("{page}/{limit}")
public Result<PageResult<PermissionVo>> getPermissionPage(
@PathVariable Integer page,
@PathVariable Integer limit) {
// ...
}
}
```
### 3. 全栈控制模式
前两个结合
## 🛡️ 安全配置
### 路径匹配策略
AntPath详情https://juejin.cn/spost/7498247273660743732
| 模式 | 示例 | 说明 |
| -------- | --------------- | ---------------- |
| 精确匹配 | `/api/user` | 完全匹配路径 |
| 通配符 | `/api/user/*` | 匹配单级路径 |
| 多级通配 | `/api/user/**` | 匹配多级路径 |
| 方法限定 | `GET /api/user` | 匹配特定HTTP方法 |
## 🧰 技术栈
### 前端
- Vue 3 + PureAdmin 模板
- 自定义权限组件
- 国际化支持
### 后端
- Spring Boot 3 + Spring Security 6
- JDK 17
- MySQL + Redis + MinIO
- Swagger + Knife4j 文档
### 开发环境
根据不懂docker 启动方式不一样
```bash
# 一键启动依赖服务
docker-compose up -d
# 新版的docker
docker compose up -d
```
#### 数据库文件
在后端文件的根目录中
![image-20241107133345299](http://129.211.31.58:9000/docs/image-20241107133345299.png)
## 后端日志文件
在后端日志文件中,使用了`logback.xml`进行格式化。然而,使用`logback.xml`后配置文件中指定的日志输出文件位置可能会失效。如果项目是通过Docker部署的想在宿主机查看日志文件需要进行文件映射。
### 使用SpringBoot
在配置文件中指定名称和目录路径即可之后使用docker映射就可以在宿主机看到日志了
如果想要用自带需要删除`logback.xml`文件
```
logging:
level:
cn.bunny.service.mapper: warn
cn.bunny.service.controller: warn
cn.bunny.service.service: warn
root: warn
pattern:
dateformat: HH:mm:ss:SSS
file:
path: "logs/${spring.application.name}"
name: "logs/${spring.application.name}"
```
### 使用logback.xml
指定目录的位置`<file>D:/logs/${datetime}/auth-server.log</file>`根据你需要的指定
```xml
<!-- 格式化 年--日 输出 -->
<timestamp key="datetime" datePattern="yyyy-MM-dd"/>
<appender name="STOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%cyan([%thread]) %yellow(%-5level) %green(%logger{100}).%boldRed(%method)-%boldMagenta(%line)- %blue(%msg%n)
</pattern>
</encoder>
</appender>
<!-- 文件日志 -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>/www/root/server/logs/${datetime}/auth-server.log</file>
<append>true</append>
<encoder>
<pattern>%date{yyyy-MM-dd HH:mm:ss} %-5level %logger{100} %method %line %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
```
> 如果按照上面搭建,日志文件会在`/www/root/server/logs`下即使使Windows系统也会存在如果docker使用的是文件映射那么日志文件会在容器相对应的位置
如果开发环境或者其他环境也需要日志,可以根据当前环境进行选择`<springProfile name="prod">`
```xml
<!-- 生产环境 -->
<springProfile name="prod">
<!-- 日志记录器业务程序INFO级别 -->
<logger name="cn.bunny" level="INFO"/>
<!-- 根日志记录器INFO级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>
```
# 项目特点
### 按钮权限显示
如果当前用户在这个页面中只有【添加】和【删除】那么页面按钮中只会显示出【添加按钮】和【删除按钮】
### 去除前后空格
后端配置了自动去除前端传递的空字符串,如果传递的内容前后有空格会自动去除前后的空格
![image-20241105215241811](http://129.211.31.58:9000/docs/image-20241105215241811.png)
### 项目接口和页面
接口地址有两个:
1. knife4j
2. swagger
接口地址://localhost:7070/doc.html#/home
![image-20241105213953503](http://129.211.31.58:9000/docs/image-20241105213953503.png)
swagger接口地址http://localhost:7070/swagger-ui/index.html
![image-20241105214100720](http://129.211.31.58:9000/docs/image-20241105214100720.png)
前端接口地址http://localhost:7000/#/welcome
![image-20241105214230389](http://129.211.31.58:9000/docs/image-20241105214230389.png)
## 登录功能
> [!warning]
>
> 可以选择邮箱登录或者是密码直接登录,两者不互用。
### 账号登录
![image-20250222184157801](http://129.211.31.58:9000/docs/image-20250222184157801.png)
#### 业务需求
- 用户输入用户名和密码进行登录
#### 实现思路
- 用户输入账号和密码和数据库中账号密码进行比对,成功后进行页面跳转
- 如果账户禁用会显示账户已封禁
> [!tip]
>
> 这里做了登录策略,可以方便以后的扩展。
>
> ![image-20250222184405761](http://129.211.31.58:9000/docs/image-20250222184405761-1740221103225-1.png)
**后端实现文件位置**
- 拦截请求为`/admin/login`的请求之后进行登录验证的判断
![image-20250222184248858](http://129.211.31.58:9000/docs/image-20250222184248858.png)
### 邮箱登录
#### 业务需求
- 用户输入邮箱账号、密码、邮箱验证码之后进行登录
#### 实现思路
- 需要验证用户输入的邮箱格式是否正确。
- 在未输入验证码的情况下输入密码会提示用户同时后端也会进行验证。如果输入了邮箱验证码但是Redis中不存在或已过期会提示邮箱验证码不存在或已过期。
- 之后对邮箱账号和密码进行判断包括邮箱验证码进行判断
## 首页功能
功能菜单,首页图表展示部分功能已经由这个模板作者设计好,其中需要注意的是,如果要查看历史消息或者是进入消息页面可以双击
### 消息功能
#### 业务需求
1. 消息页面的展示,包含删除、批量删除、选中已读和当前页面所有消息都标为已读
2. 当用户对左侧菜单点击时可以过滤出消息内容,展示不同的消息类型
3. 可以点击已读和全部进行筛选消息
4. 可以根据标题进行搜搜
5. 包含分页
#### 实现思路
1. 显示当前消息类型,用户点击时带参数请求,只带当前消息类型,不默认携带已读状态查询,然后从数据库筛选并返回结果。
2. 点击"已读"选项时,若选择"全部"之前是设置为undefined这样就不会携带参数了但是底下会有警告现在改为空字符串后端只需过滤掉空字符串即可。
3. 删除选定数据若用户选择列表并筛选出所有ID将数据传递给后端用户删除为逻辑删除
4. 全部标为已读![image-20241106131949217](http://129.211.31.58:9000/docs/image-20241106131949217.png)类似删除操作筛选出选中数据的ID然后传递给后端以标记为已读。
5. 将所有数据标记为已读当前页面前端使用map提取所有ID整合成ID列表传递给后端表示页面上所有数据已读。
6. 输入标题后,随输入变化进行搜索。
消息提醒在页面刷新时会出现这样
![image-20241229214535717](http://129.211.31.58:9000/docs/image-20241229214535717.png)
### 用户管理
#### 需求分析
1. 用户操作需要包含CURD的操作
2. 为了方便在用户中需要给出快速禁用当前用户按钮
3. 需要显示用户头像、性别、最后登录的IP地址和归属地
4. 在左侧中需要包含部分查询
5. 可以根据点击的部门查询当前部门下的用户
6. 根据用户可以强制下线、管理员可以修改用户密码、为用户分配角色
**重置密码**
重置密码需要判断当前用户密码是否是符合指定的密码格式,并且会根据当前输入密码计算得分如果当前密码复杂则得分越高那么密码强度越强
![image-20241106003256994](http://129.211.31.58:9000/docs/image-20241106003256994.png)
重置密码组件在前端的公共组件文件中
![image-20241106003426573](http://129.211.31.58:9000/docs/image-20241106003426573.png)
**分配角色**
- 给用户分配了admin角色后其他路由绑定和权限设置就不再需要了因为后端会根据admin角色在前端用户信息中设置通用权限码如`*`、`*::*`、`*::*::*`,表示前端用户可以访问所有权限并查看所有内容。
- 管理员有权对用户进行角色分配,这涉及到许多操作,包括菜单显示和接口访问权限。角色与权限相关联,角色也与菜单相关联。
- 当用户访问菜单时,会根据其角色看到其所属的菜单内容。随后,角色与权限接口相关联,根据用户的权限来决定是否显示操作按钮。后端会根据用户的权限验证其是否可以访问当前接口。
- 用户登录或刷新页面时会重新获取用户信息,用户信息中包含角色和权限信息。利用角色和权限信息与前端传递的路径进行比对判断,如果用户包含菜单角色,则可以访问。如果用户包含前端路由中的权限,则表示该权限可以访问。后端也会进行权限判断,以防止通过接口文档等方式访问。
- 分配好角色后,菜单会根据当前路由角色匹配用户角色,从而根据用户角色显示相应的菜单内容。
![image-20241106004533031](http://129.211.31.58:9000/docs/image-20241106004533031.png)
### 角色管理
角色管理包含CURD和权限分配操作
![image-20241106132548236](http://129.211.31.58:9000/docs/image-20241106132548236.png)
#### 业务需求
用户对角色进行CURD操作点击权限设置时让用户分配权限
#### 实现思路
1. 在设计的表中,如果存在相同的角色码,系统会提示用户当前角色已经存在。
2. 后端会根据角色的ID分配权限的ID列表。
![image-20241106135600255](http://129.211.31.58:9000/docs/image-20241106135600255.png)
3. 后端在角色权限表中会根据角色的ID分配权限内容。在角色权限表中会先删除当前角色所有的权限内容然后再进行权限内容的重新分配。
```java
public void assignPowersToRole(AssignPowersToRoleDto dto) {
List<Long> powerIds = dto.getPowerIds();
Long roleId = dto.getRoleId();
// 删除这个角色下所有权限
baseMapper.deleteBatchRoleIdsWithPhysics(List.of(roleId));
// 保存分配数据
List<RolePower> rolePowerList = powerIds.stream().map(powerId -> {
RolePower rolePower = new RolePower();
rolePower.setRoleId(roleId);
rolePower.setPowerId(powerId);
return rolePower;
}).toList();
saveBatch(rolePowerList);
// 找到所有和当前更新角色相同的用户
List<Long> roleIds = userRoleMapper.selectList(Wrappers.<UserRole>lambdaQuery().eq(UserRole::getRoleId, roleId))
.stream().map(UserRole::getUserId).toList();
// 根据Id查找所有用户
List<AdminUser> adminUsers = userMapper.selectList(Wrappers.<AdminUser>lambdaQuery().in(!roleIds.isEmpty(), AdminUser::getId, roleIds));
// 用户为空时不更新Redis的key
if (adminUsers.isEmpty()) return;
// 更新Redis中用户信息
List<Long> userIds = adminUsers.stream().map(AdminUser::getId).toList();
roleFactory.updateUserRedisInfo(userIds);
}
```
### 权限管理
![image-20241106135954104](http://129.211.31.58:9000/docs/image-20241106135954104.png)
![image-20241106140006176](http://129.211.31.58:9000/docs/image-20241106140006176.png)
在权限配置中,添加/修改权限时的请求地址为后端接口的请求地址,请求地址使用了【`正则表达式`】判断和【`antpath`】方式填写
#### 业务需求
1. 对权限表进行CURD操作
2. 在表格中点击新增时父级id为当前点击行的id
#### 实现思路
点击当前行父级id为当前的行的id
![image-20241106140420845](http://129.211.31.58:9000/docs/image-20241106140420845.png)
#### 权限判断实现方式
##### 后端判断方式
判断权限是否可以访问,后端实现判断逻辑
![image-20241106003921315](http://129.211.31.58:9000/docs/image-20241106003921315.png)
##### 前端判断方式
角色分配方式有下面几种想洗参考https://pure-admin.github.io/vue-pure-admin/#/permission/button/router[文档页面](https://pure-admin.github.io/pure-admin-doc/pages/routerMenu/#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%B7%AF%E7%94%B1%E7%9A%84-name-%E5%BF%85%E5%86%99-%E8%80%8C%E4%B8%94%E5%BF%85%E9%A1%BB%E5%94%AF%E4%B8%80)
1. 使用标签方式
![image-20241106004247600](http://129.211.31.58:9000/docs/image-20241106004247600.png)
2. 使用函数方式
![image-20241106004310635](http://129.211.31.58:9000/docs/image-20241106004310635.png)
3. 使用指令方式
![image-20241106005252328](http://129.211.31.58:9000/docs/image-20241106005252328.png)
在前端utils文件夹下有`auth.ts`文件里面包含了权限码信息,如果当前菜单属性中包含这个权限码表示可以访问这个权限
![image-20241106004433489](http://129.211.31.58:9000/docs/image-20241106004433489.png)
![image-20241106004500855](http://129.211.31.58:9000/docs/image-20241106004500855.png)
### 菜单管理
![image-20241106140545328](http://129.211.31.58:9000/docs/image-20241106140545328.png)
### 菜单路由
在做菜单返回时必须要了解角色和权限表
#### 需求分析
1. 从数据库中返回出所有的菜单,其中需要整合成前端所要的形式,需要包含`roles`和`auths`,及其其它参数。
2. 用户需要根据自己的角色访问不同的菜单。
3. 如果当前用户不可以访问某些按钮需要隐藏。
4. 用户通过其它手段访问如swagger、knife4j、apifox、postman这种工具访问需要做权限验证如果当前用户不满足访问这些接口后端需要拒绝。
5. 如果已经添加了菜单名称、路由等级、路由路径会提示`xxx已存在`![image-20241106132818902](http://129.211.31.58:9000/docs/image-20241106132818902.png)
6. 在数据库中为部分字段建立了唯一索引
#### 实现思路
1. 角色和权限哪些可以访问的页面交给前端,在模板中已经设计好,如果用户访问了自己看不到的菜单会出现`403`
页面;判断方式是根据后端返回的菜单中如果包含当前用户的角色就表示可以访问当前的菜单,如果用户信息中没有这个角色则表示不可以访问这个页面。
2. 页面是否可以访问只是在操作上,如果用户通过接口访问是阻止不了的,所以这时后端需要在后端中进行判断,当前的访问路径是否是被允许的,也就是这个用户是否有这个权限,权限表设计中包含了请求路径
3. 后端需要判断用户请求这个接口是否有权访问
> 整合成前端格式返回需要递归,后端根据当前用户访问的菜单需要进行递归菜单数据之后返回前端,并将这些菜单绑定的角色放置在`roles`
> 中,之后根据角色查询全新啊相关内容,要将权限内容放置在`auths`中.
>
> 如果包含子菜单需要防止在`children`数组中,后端实现时如果没有子菜单默认是空数组而不是`null`
>
> 大致如下:
>
> ```json
> {
> "menuType": 0,
> "title": "admin_user",
> "path": "/system/admin-user",
> "component": "/system/adminUser/index",
> "meta": {
> "icon": "ic:round-manage-accounts",
> "title": "admin_user",
> "rank": 2,
> "roles": [
> "admin",
> "all_page",
> "system",
> "test"
> ],
> "auths": [
> "message::updateMessage",
> "menuIcon::getMenuIconList",
> "admin::messageReceived",
> "config::getWebConfig",
> "admin::config",
> "i18n::getI18n",
> ....
> ],
> "frameSrc": ""
> },
> "children": [],
> "id": "1841803086252548097",
> "parentId": "1",
> "name": "admin_user",
> "rank": 2
> }
> ```
### 部门管理
![image-20241106140738517](http://129.211.31.58:9000/docs/image-20241106140738517.png)
![image-20241106140728748](http://129.211.31.58:9000/docs/image-20241106140728748.png)
#### 业务需求
1. 包含CURD
2. 在用户管理中可以选择对应的部门
#### 实现思路
1. CURD接口文件如下
![image-20241106140826034](http://129.211.31.58:9000/docs/image-20241106140826034.png)
2. 管理员为用户分配部门
![image-20241106140942278](http://129.211.31.58:9000/docs/image-20241106140942278.png)
### 菜单图标
![image-20241106141037894](http://129.211.31.58:9000/docs/image-20241106141037894.png)
#### 业务需求
1. 用户在菜单中可以选择存储在数据库中的图标内容
2. 包含CURD内容
#### 实现思路
后端需要返回接口格式实体类如下
```java
public class MenuIconVo extends BaseUserVo {
@Schema(name = "iconCode", title = "icon类名")
private String iconCode;
@Schema(name = "iconName", title = "icon 名称")
private String iconName;
}
```
![image-20241106141521051](http://129.211.31.58:9000/docs/image-20241106141521051.png)
前端封装好的组件
### 邮箱相关
#### 业务需求
1. 邮件用户配置CURD
2. 邮件模板CURD
3. 邮件用户中只能有一个是默认的,如果当前修改其它项需要将其它已经启用改为不启用
4. 邮件模板需要绑定邮件用户
### 实现思路
邮件模板中添加或者修改时前端需要返回所有的邮件模板用户添加或者修改时将用户ID存储在邮件模板的数据字段中
### web配置
![image-20241106142001190](http://129.211.31.58:9000/docs/image-20241106142001190.png)
#### 主题颜色
后端做了Hash值校验如果是RGB会造成颜色显示有些问题比如使用纯色的按钮想下面的这种的鼠标hover事件会变透明因为无法计算Hash值所以在修改颜色值时一定要保证是Hash值
![image-20241229214049079](http://129.211.31.58:9000/docs/image-20241229214049079.png)
![image-20241229214217968](http://129.211.31.58:9000/docs/image-20241229214217968.png)
修改完成的示例
![image-20241229214329544](http://129.211.31.58:9000/docs/image-20241229214329544.png)
### 系统监控
#### 服务监控
从SpringBoot的Actuator中获取信息页面采用响应式
![image-20241106142208794](http://129.211.31.58:9000/docs/image-20241106142208794.png)
#### 系统缓存
当前内容被SpringBoot缓存会显示在这
![image-20241106142253759](http://129.211.31.58:9000/docs/image-20241106142253759.png)
### 定时任务
采用Quarter持久化存储。
#### 示例显示
![image-20241229220003779](http://129.211.31.58:9000/docs/image-20241229220003779.png)
### 多语言管理
![image-20241106142531047](http://129.211.31.58:9000/docs/image-20241106142531047.png)
![image-20241106142544172](http://129.211.31.58:9000/docs/image-20241106142544172.png)
### 日志管理
存储在MySQL中如果想做优化可以放在MongoDB中
### 消息管理
管理员可以发送消息告诉xxx用户在主页中会显示![image-20241106142908363](http://129.211.31.58:9000/docs/image-20241106142908363.png)
之后点击时会看到消息封面、标题、简介、消息等级、消息等级内容
![image-20241106142949366](http://129.211.31.58:9000/docs/image-20241106142949366.png)
#### 消息类型
![image-20241106143008098](http://129.211.31.58:9000/docs/image-20241106143008098.png)
包含CURD用户编辑消息发送时可以在选择
![image-20241106144017015](http://129.211.31.58:9000/docs/image-20241106144017015.png)
同时在用户消息栏中也会显示对应内容,前端判断逻辑如下
![image-20241106144146081](http://129.211.31.58:9000/docs/image-20241106144146081.png)
#### 消息编辑
提供md编辑器和富文本编辑器
消息接受用户,如果不填写表示全部的用户,填写后会根据填写的内容存储在用户接受表中
消息等级是显示消息样式颜色,文字内容为消息简介内容
#### 消息接收管理
根据所选的接受用户会出现在下面的用户接受表中,可以对当前用户是否已读进行修改
#### 消息发送管理
之前编辑过的消息都会在这
# 环境部署
使用Docker进行部署后端接口地址以`/api`开头,前端默认请求前缀为`/api`,因此在请求时需要进行替换。详细内容请参考以下【项目部署】说明。
## 前端部署
运行`pnpm build`
`dockerfile`中暴露端口要和生产环境的端口号保持一致
### 使用http协议
如果不使用https需要将下面内容注释
![image-20241108152526666](http://129.211.31.58:9000/docs/auth/image-20241108152526666.png)
对外暴露端口改为`80`或者你自己喜欢的端口
![image-20241108154244339](http://129.211.31.58:9000/docs/auth/image-20241108154244339.png)
#### NGINX配置
将NGINX配置修改为以下内容如果你的暴露的端口和我一样是`80`
![image-20241108154344561](http://129.211.31.58:9000/docs/auth/image-20241108154344561.png)
#### 运行docker文件
![image-20241108151726981](http://129.211.31.58:9000/docs/auth/image-20241108151726981.png)
![image-20241108151940158](http://129.211.31.58:9000/docs/auth/image-20241108151940158.png)
> 命令预览
>
> ```sh
> docker build -f Dockerfile -t bunny_auth_web:1.0.0 . && docker run -p 80:443 -p 443:443 --name bunny_auth_web --restart always bunny_auth_web:1.0.0
> ```
## 后端部署
### 注意事项
如果需要打包需要手动创建生产环境文件:`application-prod.yml`
![image-20241117202134939](http://129.211.31.58:9000/docs/image-20241117202134939.png)
### 环境设置
开发环境环境:对外暴露的端口是`7070`
生产环境:对外暴露的端口是`8000`
需要先打包,打包完成后会在目录下生成对应的`target`相关文件
```bash
# 开发环境
mvn clean package -Pprod -DskipTests
# 测试环境
mvn clean package -Ptest -DskipTests
```
**:star: 如果不需要内置文件操作什么的这一步可以忽略**
![image-20241108153705104](http://129.211.31.58:9000/docs/auth/image-20241108153705104.png)
这个文件夹是必须的
![image-20241108153750097](http://129.211.31.58:9000/docs/auth/image-20241108153750097.png)
## 📚 最佳实践
1. **注解规范**
```java
@Tag(name = "模块名称", description = "模块描述")
@Operation(summary = "接口摘要", tags = {"权限码"})
// 或者
@Operation(summary = "接口摘要", tags = "权限码")
```
2. **权限码设计**
- 模块::操作 (如 `user::create`)
- 分层级设计 (如 `system:user:update`)
3. **批量操作**
- 使用 Excel/JSON 管理大量权限配置
- 定期备份权限配置
## 🌟 项目优势
1. **真正的动态控制** - 无需硬编码权限逻辑
2. **灵活的数据导入** - 支持多种文件格式
3. **细粒度控制** - 从页面到按钮的多层级权限
4. **现代化技术栈** - 基于最新 Spring 生态
5. **开箱即用** - 提供完整 Docker 部署方案
## 📌 注意事项
1. 多语言更新会完全替换现有配置
2. 生产环境建议禁用 Swagger 端点
3. 复杂权限建议使用 Excel 批量管理
## 📈 后续规划
- [ ] 权限级别拖拽
- [ ] 权限树型结构动态添加、更新、删除
- [ ] 用户设置持久化存储到数据库
- [ ] 权限弹窗页面优化
- [ ] 后端文档注释完善
- [ ] 系统监控后端返回403停止请求
## 前后端接口规范
### 前端示例规范
| **操作** | **API 层** | **Pinia 层** |
| :------- | :------------ | :-------------- |
| 查询单个 | `getUser` | `loadUser` |
| 查询列表 | `getUserList` | `loadUserList` |
| 分页查询 | `getUserPage` | `fetchUserPage` |
| 新增数据 | `createUser` | `addUser` |
| 更新数据 | `updateUser` | `editUser` |
| 删除数据 | `deleteUser` | `removeUser` |
### 后端接口示例规范
遵循Restful
| **操作** | **RESTful** |
| :------- | :-------------------------- |
| 查询列表 | `GET /users` |
| 分页查询 | `GET /users/{page}/{limit}` |
| 查询单个 | `GET /users/{id}` |
| 新增 | `POST /users` |
| 更新 | `PUT /users/{id}` |
| 删除 | `DELETE /users/{id}` |
![wx_alipay](./images/wx_alipay.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
images/wx_alipay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

View File

@ -26,7 +26,7 @@
c = document.createElement('div');
(c.innerHTML = e._iconfont_svg_string_2208059),
(c = c.getElementsByTagName('svg')[0]) &&
(c.setAttribute('aria-hidden', 'true'),
(c.setAttribute('aria-hidden', 'false'),
(c.style.position = 'absolute'),
(c.style.width = 0),
(c.style.height = 0),

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img"
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="false" role="img"
width="1em" height="1em" viewBox="0 0 24 24" style="outline: none;">
<path fill="currentColor"
d="M14 8.947L22 14v2l-8-2.526v5.36l3 1.666V22l-4.5-1L8 22v-1.5l3-1.667v-5.36L3 16v-2l8-5.053V3.5a1.5 1.5 0 0 1 3 0v5.447Z"></path>

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 319 B

View File

@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--ant-design" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="false" class="iconify iconify--ant-design" viewBox="0 0 1024 1024">
<path fill="currentColor"
d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8"/>
</svg>

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 365 B

View File

@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="false" class="re-screen" color="#00000073" viewBox="0 0 16 16">
<path fill="currentColor"
d="M3.5 4H1V3h2V1h1v2.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/>
</svg>

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 341 B

View File

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="false" class="re-screen" color="#00000073" viewBox="0 0 16 16">
<path fill="currentColor" d="M3 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 302 B

After

Width:  |  Height:  |  Size: 308 B

View File

@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="globalization" viewBox="0 0 512 512"><path fill="currentColor" d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="false" class="globalization" viewBox="0 0 512 512">
<path fill="currentColor"
d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"/>
</svg>

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 840 B

View File

@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--mdi" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="false" class="iconify iconify--mdi" viewBox="0 0 24 24">
<path fill="currentColor"
d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"/>
</svg>

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 393 B

View File

@ -1 +1,4 @@
<svg width="32" height="32" fill="currentColor" aria-hidden="true" data-icon="holder" viewBox="64 64 896 896"><path d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97m0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0m0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0M300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0"/></svg>
<svg width="32" height="32" fill="currentColor" aria-hidden="false" data-icon="holder" viewBox="64 64 896 896">
<path
d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97m0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0m0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0M300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0"/>
</svg>

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 402 B

View File

@ -25,7 +25,7 @@ export default defineComponent({
'svg',
{
class: 'icon-svg',
'aria-hidden': true,
'aria-hidden': false,
},
{
default: () => [

View File

@ -38,7 +38,7 @@
(t.innerHTML = i),
(i = null),
(t = t.getElementsByTagName('svg')[0]) &&
(t.setAttribute('aria-hidden', 'true'),
(t.setAttribute('aria-hidden', 'false'),
(t.style.position = 'absolute'),
(t.style.width = 0),
(t.style.height = 0),

View File

@ -5,11 +5,12 @@ import Check from '@iconify-icons/ep/check';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { delay, getKeyList, handleTree, subBefore, useResizeObserver } from '@pureadmin/utils';
import { usePermissionStore } from '@/store/system/power';
import { powerCascadeProps } from '@/views/system/permission/utils';
import { useRoleStore } from '@/store/system/role';
import { powerCascadeProps } from '@/views/system/permission/utils';
const powerStore = usePermissionStore();
const roleStore = useRoleStore();
//
const isExpandAll = ref(false);
//