|
@@ -0,0 +1,3586 @@
|
|
|
+# 智能数据分析系统设计开发与实现文档
|
|
|
+
|
|
|
+- 基于RBAC的权限管理设计
|
|
|
+- 用户管理设计
|
|
|
+- 图表管理设计
|
|
|
+
|
|
|
+## 一.开发背景
|
|
|
+
|
|
|
+随着大数据时代的到来,数据分析变得越来越重要。
|
|
|
+
|
|
|
+智能数据分析平台旨在为用户提供一个简单易用、功能强大的数据分析工具,帮助用户从海量数据中提取有价值的信息,并通过图表直观展示。
|
|
|
+
|
|
|
+智能数据分析平台是一个基于Spring Boot和Vue.js的前后端分离应用,旨在为用户提供一个简单易用的数据分析工具。
|
|
|
+
|
|
|
+平台通过JWT实现身份验证,使用RBAC进行权限管理,并提供图表生成和管理功能。
|
|
|
+
|
|
|
+## 二.需求分析
|
|
|
+
|
|
|
+### 2.1 功能需求
|
|
|
+
|
|
|
+#### 2.1.1 数据分析接口
|
|
|
+
|
|
|
+- **数据分析**:分析用户上传的数据,得出结论,并将数据可视化
|
|
|
+- **接口限流**:使用令牌桶算法限制接口访问频率,防止滥用。
|
|
|
+- **异步处理**:通过线程池异步处理数据分析任务,提高系统响应速度。
|
|
|
+
|
|
|
+#### 2.1.2 权限控制
|
|
|
+
|
|
|
+- **基于RBAC的权限控制**:允许指定的角色访问指定的资源
|
|
|
+
|
|
|
+#### 2.1.3 用户管理
|
|
|
+
|
|
|
+- **用户注册与登录**:用户可以注册新账户并登录系统。
|
|
|
+- **用户信息管理**:用户可以查看和更新自己的信息。
|
|
|
+
|
|
|
+#### 2.1.4 图表管理
|
|
|
+
|
|
|
+- **图表生成**:用户可以上传数据,系统根据分析目标生成图表。
|
|
|
+- **图表查看与编辑**:用户可以查看和删除图表。
|
|
|
+
|
|
|
+### 2.2 非功能需求
|
|
|
+
|
|
|
+#### 2.2.1 性能需求
|
|
|
+
|
|
|
+- 系统应能够处理大量并发请求,响应时间不超过2秒。
|
|
|
+
|
|
|
+#### 2.2.2 安全需求
|
|
|
+
|
|
|
+- 系统必须实现用户身份验证和授权,保护数据安全。
|
|
|
+
|
|
|
+#### 2.2.3 可用性需求
|
|
|
+
|
|
|
+- 系统应提供友好的用户界面,确保用户能够轻松使用各项功能。
|
|
|
+
|
|
|
+## 三.需求设计
|
|
|
+
|
|
|
+### 3.1 架构设计
|
|
|
+
|
|
|
+系统采用B/S架构,前端使用Vue.js构建用户界面,后端使用Spring Boot构建RESTful API。
|
|
|
+
|
|
|
+- **后端**:Spring Boot、Spring Security、MyBatis Plus、Redisson、JWT、hutool等
|
|
|
+- **前端**:Vue.js、Element Plus、ECharts等
|
|
|
+- **数据库**:MySQL
|
|
|
+- **缓存**:Redis
|
|
|
+
|
|
|
+### 3.2 数据库设计
|
|
|
+
|
|
|
+数据库使用MySQL,设计以下表结构:
|
|
|
+
|
|
|
+#### 3.2.1 用户表
|
|
|
+
|
|
|
+- **用户ID**:唯一标识。
|
|
|
+- **用户名**:用户昵称。
|
|
|
+- **密码**:加密存储。
|
|
|
+- **角色**:用户角色。
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE if NOT EXISTS `user` (
|
|
|
+ `id` BIGINT NOT NULL PRIMARY KEY COMMENT '用户ID',
|
|
|
+ `user_account` VARCHAR(255) NOT NULL COMMENT '用户账号',
|
|
|
+ `user_password` VARCHAR(255) NOT NULL COMMENT '用户密码',
|
|
|
+ `user_name` VARCHAR(255) NOT NULL COMMENT '用户名称',
|
|
|
+ `user_avatar` VARCHAR(1024) DEFAULT NULL COMMENT '用户头像',
|
|
|
+ `user_role` CHAR(32) NOT NULL DEFAULT '用户' COMMENT '用户角色',
|
|
|
+ `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
|
+ `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
|
+ `delete_flag` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '删除标志,0:未删除,1:已删除'
|
|
|
+) COMMENT='用户表';
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+#### 3.2.2 图表表
|
|
|
+
|
|
|
+- **图表ID**:唯一标识。
|
|
|
+- **图表名称**:用户定义的图表名称。
|
|
|
+- **分析目标**:图表展示的数据目标。
|
|
|
+- **图表数据**:图表的JSON配置数据。
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE if NOT EXISTS `chart` (
|
|
|
+ `id` BIGINT NOT NULL PRIMARY KEY COMMENT '图表ID',
|
|
|
+ `name` varchar(128) NULL COMMENT '图表名称',
|
|
|
+ `analysis_target` TEXT NOT NULL COMMENT '分析目标',
|
|
|
+ `chart_data` TEXT NOT NULL COMMENT '图标数据',
|
|
|
+ `chart_type` VARCHAR(255) NOT NULL COMMENT '图标类型',
|
|
|
+ `generated_chart_data` TEXT COMMENT '生成的图表数据',
|
|
|
+ `analysis_conclusion` TEXT COMMENT '生成的分析结论',
|
|
|
+ `user_id` BIGINT NOT NULL COMMENT '创建用户ID',
|
|
|
+ `state` CHAR(32) NOT NULL DEFAULT '等待中' COMMENT '图表状态,等待中,生成中,成功,失败',
|
|
|
+ `execute_message` TEXT COMMENT '执行信息',
|
|
|
+ `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
|
+ `updated_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
|
+ `delete_flag` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '删除标志,0:未删除,1:已删除'
|
|
|
+) COMMENT='图表表';
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 3.3 接口设计
|
|
|
+
|
|
|
+使用Swagger文档化API接口。
|
|
|
+
|
|
|
+#### 3.3.1 用户相关接口
|
|
|
+
|
|
|
+- `POST /user/login`:用户登录。
|
|
|
+- `POST /user/register`:用户注册。
|
|
|
+- `GET /user/getUSerInfo`:获取用户信息
|
|
|
+- `GET /user/page`:用户信息分页
|
|
|
+- `POST /user`:新增用户
|
|
|
+- `PUT /user`:更新用户信息
|
|
|
+- `GET /user/getUserBiId/{id}`:获取用户信息
|
|
|
+- `DELETE /user`/{ids}:删除用户
|
|
|
+
|
|
|
+#### login
|
|
|
+
|
|
|
+**接口地址**:`/user/login`
|
|
|
+
|
|
|
+**请求方式**:`POST`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/json`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+**接口描述**:用户登录
|
|
|
+
|
|
|
+
|
|
|
+**请求示例**:
|
|
|
+
|
|
|
+
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "createTime": "",
|
|
|
+ "deleteFlag": 0,
|
|
|
+ "id": 0,
|
|
|
+ "updateTime": "",
|
|
|
+ "userAccount": "",
|
|
|
+ "userAvatar": "",
|
|
|
+ "userName": "",
|
|
|
+ "userPassword": "",
|
|
|
+ "userRole": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| ------------------------ | -------- | -------- | -------- | ----------------- | ------ |
|
|
|
+| user | user | body | true | User | User |
|
|
|
+|   createTime | | | false | string(date-time) | |
|
|
|
+|   deleteFlag | | | false | integer(int32) | |
|
|
|
+|   id | | | false | integer(int64) | |
|
|
|
+|   updateTime | | | false | string(date-time) | |
|
|
|
+|   userAccount | | | false | string | |
|
|
|
+|   userAvatar | | | false | string | |
|
|
|
+|   userName | | | false | string | |
|
|
|
+|   userPassword | | | false | string | |
|
|
|
+|   userRole | | | false | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 201 | Created | |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### resister
|
|
|
+
|
|
|
+
|
|
|
+**接口地址**:`/user/register`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`POST`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/json`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+**接口描述**:用户注册
|
|
|
+
|
|
|
+
|
|
|
+**请求示例**:
|
|
|
+
|
|
|
+
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "createTime": "",
|
|
|
+ "deleteFlag": 0,
|
|
|
+ "id": 0,
|
|
|
+ "updateTime": "",
|
|
|
+ "userAccount": "",
|
|
|
+ "userAvatar": "",
|
|
|
+ "userName": "",
|
|
|
+ "userPassword": "",
|
|
|
+ "userRole": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| ------------------------ | -------- | -------- | -------- | ----------------- | ------ |
|
|
|
+| user | user | body | true | User | User |
|
|
|
+|   createTime | | | false | string(date-time) | |
|
|
|
+|   deleteFlag | | | false | integer(int32) | |
|
|
|
+|   id | | | false | integer(int64) | |
|
|
|
+|   updateTime | | | false | string(date-time) | |
|
|
|
+|   userAccount | | | false | string | |
|
|
|
+|   userAvatar | | | false | string | |
|
|
|
+|   userName | | | false | string | |
|
|
|
+|   userPassword | | | false | string | |
|
|
|
+|   userRole | | | false | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 201 | Created | |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### getUserInfo
|
|
|
+
|
|
|
+
|
|
|
+**接口地址**:`/user/getUserInfo`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`GET`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/x-www-form-urlencoded`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+暂无
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### page
|
|
|
+
|
|
|
+
|
|
|
+**接口地址**:`/user/page`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`GET`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/x-www-form-urlencoded`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| ----------- | ----------- | -------- | -------- | -------------- | ------ |
|
|
|
+| currentPage | currentPage | query | false | integer(int32) | |
|
|
|
+| pageSize | pageSize | query | false | integer(int32) | |
|
|
|
+| userAccount | userAccount | query | false | string | |
|
|
|
+| userName | userName | query | false | string | |
|
|
|
+| userRole | userRole | query | false | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### addUser
|
|
|
+
|
|
|
+
|
|
|
+**接口地址**:`/user`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`POST`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/json`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求示例**:
|
|
|
+
|
|
|
+
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "id": 0,
|
|
|
+ "userAccount": "",
|
|
|
+ "userAvatar": "",
|
|
|
+ "userName": "",
|
|
|
+ "userPassword": "",
|
|
|
+ "userRole": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| ------------------------ | -------- | -------- | -------- | -------------- | ------- |
|
|
|
+| userDTO | userDTO | body | true | UserDTO | UserDTO |
|
|
|
+|   id | | | false | integer(int64) | |
|
|
|
+|   userAccount | | | false | string | |
|
|
|
+|   userAvatar | | | false | string | |
|
|
|
+|   userName | | | false | string | |
|
|
|
+|   userPassword | | | false | string | |
|
|
|
+|   userRole | | | false | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 201 | Created | |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### updateUser
|
|
|
+
|
|
|
+
|
|
|
+**接口地址**:`/user`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`PUT`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/json`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求示例**:
|
|
|
+
|
|
|
+
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "id": 0,
|
|
|
+ "userAccount": "",
|
|
|
+ "userAvatar": "",
|
|
|
+ "userName": "",
|
|
|
+ "userPassword": "",
|
|
|
+ "userRole": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| ------------------------ | -------- | -------- | -------- | -------------- | ------- |
|
|
|
+| userDTO | userDTO | body | true | UserDTO | UserDTO |
|
|
|
+|   id | | | false | integer(int64) | |
|
|
|
+|   userAccount | | | false | string | |
|
|
|
+|   userAvatar | | | false | string | |
|
|
|
+|   userName | | | false | string | |
|
|
|
+|   userPassword | | | false | string | |
|
|
|
+|   userRole | | | false | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 201 | Created | |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### getUserById
|
|
|
+
|
|
|
+
|
|
|
+**接口地址**:`/user/{id}`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`GET`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/x-www-form-urlencoded`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| -------- | -------- | -------- | -------- | -------------- | ------ |
|
|
|
+| id | id | path | true | integer(int64) | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### deleteUserById
|
|
|
+
|
|
|
+
|
|
|
+**接口地址**:`/user/{ids}`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`DELETE`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/x-www-form-urlencoded`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| -------- | -------- | -------- | -------- | -------- | ------ |
|
|
|
+| ids | ids | path | true | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 204 | No Content | |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.3.2 图表相关接口
|
|
|
+
|
|
|
+- `POST /chart/generateChartByAI`:生成图表。
|
|
|
+- `GET /chart/getChartById/{id}`:获取图表详情。
|
|
|
+- `GET /chart/list`:获取图表列表
|
|
|
+- `POST /chart/add`:新增图表
|
|
|
+- `PUT /chart/update`:更新图表
|
|
|
+- `DELETE /chart/{ids}`:删除图表
|
|
|
+
|
|
|
+#### generateChartByAI
|
|
|
+
|
|
|
+**接口地址**:`/chart/generateChartByAI`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`POST`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`multipart/form-data`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| -------------- | -------- | -------- | -------- | -------- | ------ |
|
|
|
+| analysisTarget | | query | false | string | |
|
|
|
+| chartData | | query | false | string | |
|
|
|
+| chartType | | query | false | string | |
|
|
|
+| isAsynchronism | | query | false | boolean | |
|
|
|
+| name | | query | false | string | |
|
|
|
+| file | file | formData | false | file | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 201 | Created | |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### getChartById
|
|
|
+
|
|
|
+**接口地址**:`/chart/getChartById/{id}`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`GET`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/x-www-form-urlencoded`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| -------- | -------- | -------- | -------- | -------------- | ------ |
|
|
|
+| id | id | path | true | integer(int64) | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### page
|
|
|
+
|
|
|
+**接口地址**:`/chart/list`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`GET`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/x-www-form-urlencoded`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| -------- | -------- | -------- | -------- | -------------- | ------ |
|
|
|
+| UserId | UserId | query | false | integer(int64) | |
|
|
|
+| name | name | query | false | string | |
|
|
|
+| pageNum | pageNum | query | false | integer(int32) | |
|
|
|
+| pageSize | pageSize | query | false | integer(int32) | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### addChart
|
|
|
+
|
|
|
+**接口地址**:`/chart/add`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`POST`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/json`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求示例**:
|
|
|
+
|
|
|
+
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "analysisConclusion": "",
|
|
|
+ "analysisTarget": "",
|
|
|
+ "chartData": "",
|
|
|
+ "chartType": "",
|
|
|
+ "createdTime": "",
|
|
|
+ "deleteFlag": 0,
|
|
|
+ "executeMessage": "",
|
|
|
+ "generatedChartData": "",
|
|
|
+ "id": 0,
|
|
|
+ "name": "",
|
|
|
+ "state": "",
|
|
|
+ "updatedTime": "",
|
|
|
+ "userId": 0
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| ------------------------------ | -------- | -------- | -------- | ----------------- | ------ |
|
|
|
+| chart | chart | body | true | Chart | Chart |
|
|
|
+|   analysisConclusion | | | false | string | |
|
|
|
+|   analysisTarget | | | false | string | |
|
|
|
+|   chartData | | | false | string | |
|
|
|
+|   chartType | | | false | string | |
|
|
|
+|   createdTime | | | false | string(date-time) | |
|
|
|
+|   deleteFlag | | | false | integer(int32) | |
|
|
|
+|   executeMessage | | | false | string | |
|
|
|
+|   generatedChartData | | | false | string | |
|
|
|
+|   id | | | false | integer(int64) | |
|
|
|
+|   name | | | false | string | |
|
|
|
+|   state | | | false | string | |
|
|
|
+|   updatedTime | | | false | string(date-time) | |
|
|
|
+|   userId | | | false | integer(int64) | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 201 | Created | |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### updateChart
|
|
|
+
|
|
|
+
|
|
|
+**接口地址**:`/chart/update`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`PUT`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/json`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求示例**:
|
|
|
+
|
|
|
+
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "analysisConclusion": "",
|
|
|
+ "analysisTarget": "",
|
|
|
+ "chartData": "",
|
|
|
+ "chartType": "",
|
|
|
+ "createdTime": "",
|
|
|
+ "deleteFlag": 0,
|
|
|
+ "executeMessage": "",
|
|
|
+ "generatedChartData": "",
|
|
|
+ "id": 0,
|
|
|
+ "name": "",
|
|
|
+ "state": "",
|
|
|
+ "updatedTime": "",
|
|
|
+ "userId": 0
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| ------------------------------ | -------- | -------- | -------- | ----------------- | ------ |
|
|
|
+| chart | chart | body | true | Chart | Chart |
|
|
|
+|   analysisConclusion | | | false | string | |
|
|
|
+|   analysisTarget | | | false | string | |
|
|
|
+|   chartData | | | false | string | |
|
|
|
+|   chartType | | | false | string | |
|
|
|
+|   createdTime | | | false | string(date-time) | |
|
|
|
+|   deleteFlag | | | false | integer(int32) | |
|
|
|
+|   executeMessage | | | false | string | |
|
|
|
+|   generatedChartData | | | false | string | |
|
|
|
+|   id | | | false | integer(int64) | |
|
|
|
+|   name | | | false | string | |
|
|
|
+|   state | | | false | string | |
|
|
|
+|   updatedTime | | | false | string(date-time) | |
|
|
|
+|   userId | | | false | integer(int64) | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 201 | Created | |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+| 404 | Not Found | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### deleteChartById
|
|
|
+
|
|
|
+**接口地址**:`/chart/{ids}`
|
|
|
+
|
|
|
+
|
|
|
+**请求方式**:`DELETE`
|
|
|
+
|
|
|
+
|
|
|
+**请求数据类型**:`application/x-www-form-urlencoded`
|
|
|
+
|
|
|
+
|
|
|
+**响应数据类型**:`*/*`
|
|
|
+
|
|
|
+
|
|
|
+**接口描述**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+**请求参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
|
|
|
+| -------- | -------- | -------- | -------- | -------- | ------ |
|
|
|
+| ids | ids | path | true | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应状态**:
|
|
|
+
|
|
|
+
|
|
|
+| 状态码 | 说明 | schema |
|
|
|
+| ------ | ------------ | -------------- |
|
|
|
+| 200 | OK | ResponseResult |
|
|
|
+| 204 | No Content | |
|
|
|
+| 401 | Unauthorized | |
|
|
|
+| 403 | Forbidden | |
|
|
|
+
|
|
|
+
|
|
|
+**响应参数**:
|
|
|
+
|
|
|
+
|
|
|
+| 参数名称 | 参数说明 | 类型 | schema |
|
|
|
+| -------- | -------- | -------------- | -------------- |
|
|
|
+| code | | integer(int32) | integer(int32) |
|
|
|
+| data | | object | |
|
|
|
+| msg | | string | |
|
|
|
+
|
|
|
+
|
|
|
+**响应示例**:
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": {},
|
|
|
+ "msg": ""
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.4 安全设计
|
|
|
+
|
|
|
+使用JWT进行用户认证,Spring Security实现权限控制。
|
|
|
+
|
|
|
+## 4. 功能实现
|
|
|
+
|
|
|
+### 4.1 用户管理
|
|
|
+
|
|
|
+- **登录与注册**:用户可以通过UserController进行登录和注册,系统使用JWT进行身份验证。
|
|
|
+
|
|
|
+- **权限控制**:基于角色的访问控制,使用Spring Security和自定义的权限注解实现。
|
|
|
+
|
|
|
+#### 后端接口实现
|
|
|
+
|
|
|
+##### 用户登录(Login)
|
|
|
+
|
|
|
+- **接口**:`POST /user/login`
|
|
|
+- **功能**:用户登录,验证账号和密码。
|
|
|
+- **返回**:登录成功返回JWT令牌和用户信息,失败返回错误信息。
|
|
|
+
|
|
|
+UserController
|
|
|
+
|
|
|
+当客户端向`/login`路径发送POST请求并包含用户信息(如用户名和密码)时,执行登录逻辑,并返回一个包含登录结果的`ResponseResult`对象。
|
|
|
+
|
|
|
+```java
|
|
|
+@PostMapping("/login")
|
|
|
+public ResponseResult login(@RequestBody User user){
|
|
|
+ return userService.login(user);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+UserServce
|
|
|
+
|
|
|
+实现了用户登录的完整流程,包括用户认证、JWT Token的生成、权限处理(待实现)、用户信息的存储和登录成功的响应。
|
|
|
+
|
|
|
+Spring Security和JWT结合使用的用户登录实现。
|
|
|
+
|
|
|
+1. `public ResponseResult login(User user)`:这是一个公共方法,返回类型是`ResponseResult`,参数是一个`User`对象。
|
|
|
+2. 认证过程:
|
|
|
+ - 创建`UsernamePasswordAuthenticationToken`对象,包含用户的账号和密码。
|
|
|
+ - 使用`authenticationManager.authenticate(authenticationToken)`进行认证,如果认证失败,则抛出`SystemException`异常,异常中包含登录错误的枚举值。
|
|
|
+3. 生成JWT Token:
|
|
|
+ - 如果认证成功,从认证结果中获取`LoginUserDetails`对象,这是一个包含用户详细信息的Spring Security认证主体对象。
|
|
|
+ - 从`LoginUserDetails`中获取用户ID,并使用`JwtUtil.createJWT(id.toString())`方法生成一个JWT Token。
|
|
|
+4. 权限处理(待办事项):
|
|
|
+ - 代码中有一个`//todo:权限`注释,表示这里应该添加权限相关的处理逻辑,但目前尚未实现。
|
|
|
+5. 存入Redis:
|
|
|
+ - 将`LoginUserDetails`对象存储到Redis缓存中,键为`SystemConstants.LOGIN_USER_REDIS_KEY`加上用户ID。
|
|
|
+6. 返回结果:
|
|
|
+ - 创建一个`Map`对象来存储返回的数据。
|
|
|
+ - 创建一个`UserInfo`对象,包含用户的账号、姓名、头像和角色信息。
|
|
|
+ - 将JWT Token和`UserInfo`对象放入`Map`中。
|
|
|
+ - 使用`ResponseResult.okResult(map)`方法返回一个包含登录成功状态和数据的`ResponseResult`对象。
|
|
|
+
|
|
|
+```java
|
|
|
+@Override
|
|
|
+ public ResponseResult login(User user) {
|
|
|
+ //认证
|
|
|
+ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserAccount(), user.getUserPassword());
|
|
|
+ Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
|
|
+ if (Objects.isNull(authentication)){
|
|
|
+ throw new SystemException(ResponseResult.AppHttpCodeEnum.LOGIN_ERROR);
|
|
|
+ }
|
|
|
+ //生成token
|
|
|
+ LoginUserDetails principal = (LoginUserDetails) authentication.getPrincipal();
|
|
|
+ Long id = principal.getUser().getId();
|
|
|
+ String jwt = JwtUtil.createJWT(id.toString());
|
|
|
+ //todo:权限
|
|
|
+ //存入redis
|
|
|
+ redisCache.setCacheObject(SystemConstants.LOGIN_USER_REDIS_KEY+id,principal);
|
|
|
+ //返回结果
|
|
|
+ Map<String,Object> map = new HashMap<>();
|
|
|
+ UserInfo userInfo = new UserInfo(principal.getUser().getUserAccount(),principal.getUser().getUserName(),principal.getUser().getUserAvatar(),principal.getUser().getUserRole());
|
|
|
+ map.put("token",jwt);
|
|
|
+ map.put("userInfo",userInfo);
|
|
|
+ return ResponseResult.okResult(map);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+##### 用户注册(Register)
|
|
|
+
|
|
|
+- **接口**:`POST /user/register`
|
|
|
+- **功能**:用户注册,创建新用户。
|
|
|
+- **返回**:注册成功返回成功消息,失败返回错误信息。
|
|
|
+
|
|
|
+UserController
|
|
|
+
|
|
|
+当客户端向`/register`路径发送POST请求并包含用户信息(如用户名、密码等)时,这个方法会被调用,执行注册逻辑,并返回一个包含注册结果的`ResponseResult`对象
|
|
|
+
|
|
|
+```java
|
|
|
+ @PostMapping("/register")
|
|
|
+ public ResponseResult resister(@RequestBody User user){
|
|
|
+ return userService.register(user);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+UserServce
|
|
|
+
|
|
|
+实现了用户注册的核心流程,包括检查账号是否存在、密码加密、生成唯一ID、保存用户信息到数据库,并返回注册成功的响应。
|
|
|
+
|
|
|
+1. `public ResponseResult register(User user)`:这是一个公共方法,返回类型是`ResponseResult`,参数是一个`User`对象。
|
|
|
+2. 检查用户账号是否存在:
|
|
|
+ - 调用`userAccountExist`方法检查传入的`user`对象中的账号是否已经存在。
|
|
|
+ - 如果账号已存在,则抛出`SystemException`异常,异常中包含用户名已存在的枚举值。
|
|
|
+3. 密码加密:
|
|
|
+ - 使用`passwordEncoder.encode`方法对用户密码进行加密处理。
|
|
|
+ - 将加密后的密码设置回`user`对象的`userPassword`属性。
|
|
|
+4. 生成ID:
|
|
|
+ - 使用`IdUtil.getSnowflake(1, 1).nextId()`方法生成一个唯一的ID(基于Snowflake算法)。
|
|
|
+ - 将生成的ID设置为`user`对象的`id`属性。
|
|
|
+5. 存入数据库:
|
|
|
+ - 调用`save`方法将`user`对象保存到数据库中。
|
|
|
+6. 返回结果:
|
|
|
+ - 使用`ResponseResult.okResult()`方法返回一个表示注册成功的`ResponseResult`对象。
|
|
|
+
|
|
|
+```java
|
|
|
+ @Override
|
|
|
+ public ResponseResult register(User user) {
|
|
|
+ //用户账号是否存在
|
|
|
+ if (userAccountExist(user.getUserAccount())){
|
|
|
+ throw new SystemException(ResponseResult.AppHttpCodeEnum.USERNAME_EXIST);
|
|
|
+ }
|
|
|
+ //密码加密
|
|
|
+ user.setUserPassword(passwordEncoder.encode(user.getUserPassword()));
|
|
|
+ //生成ID
|
|
|
+ long id = IdUtil.getSnowflake(1, 1).nextId();
|
|
|
+ user.setId(id);
|
|
|
+ //存入数据库
|
|
|
+ save(user);
|
|
|
+ return ResponseResult.okResult();
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+##### 获取用户信息(GetUserInfo)
|
|
|
+
|
|
|
+- **接口**:`GET /user/getUserInfo`
|
|
|
+- **功能**:获取当前登录用户的信息。
|
|
|
+- **返回**:用户信息。
|
|
|
+
|
|
|
+UserController
|
|
|
+
|
|
|
+获取当前登录用户信息的接口,只有具有“用户”角色的用户才能访问
|
|
|
+
|
|
|
+```java
|
|
|
+ @PreAuthorize("@ps.hasRole('用户')")
|
|
|
+ @GetMapping("/getUserInfo")
|
|
|
+ public ResponseResult getUserInfo(){
|
|
|
+ LoginUserDetails loginUser = SecurityUtils.getLoginUser();
|
|
|
+ if (Objects.isNull(loginUser)){
|
|
|
+ return ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.NEED_LOGIN);
|
|
|
+ }
|
|
|
+ User user = loginUser.getUser();
|
|
|
+ user.setUserPassword("");
|
|
|
+ return ResponseResult.okResult(user);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+##### 用户分页列表(UserPage)
|
|
|
+
|
|
|
+- **接口**:`GET /user/page`
|
|
|
+- **功能**:分页查询用户列表。
|
|
|
+- **参数**:当前页码、页大小、用户名、账号、角色。
|
|
|
+- **返回**:用户列表和分页信息。
|
|
|
+
|
|
|
+Usercontroller
|
|
|
+
|
|
|
+一个分页查询用户的接口,只有具有“管理员”角色的用户才能访问。它记录请求信息,执行分页查询,然后返回查询结果
|
|
|
+
|
|
|
+```java
|
|
|
+ @PreAuthorize("@ps.hasRole('管理员')")
|
|
|
+ @GetMapping("/page")
|
|
|
+ @ResponseBody
|
|
|
+ public ResponseResult page(Integer currentPage,Integer pageSize,String userName,String userAccount,String userRole){
|
|
|
+ log.info("currentPage:{},pageSize:{},userName:{},userAccount:{}",currentPage,pageSize,userName,userAccount);
|
|
|
+ PageVO page = userService.pageByUsernameAndUseraccount(currentPage, pageSize, userName, userAccount,userRole);
|
|
|
+ return ResponseResult.okResult(page);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+UserServce
|
|
|
+
|
|
|
+实现了一个分页查询用户信息的功能,它根据用户名、账号和角色进行过滤,并将查询结果转换为视图对象,最后返回一个包含分页信息的视图对象
|
|
|
+
|
|
|
+1. `public PageVO pageByUsernameAndUseraccount(...)`:这是一个公共方法,返回类型是`PageVO`,参数包括当前页码`currentPage`、页面大小`pageSize`以及用于过滤的用户名`userName`、账号`userAccount`和角色`userRole`。
|
|
|
+2. 初始化用户视图对象列表:
|
|
|
+ - 创建一个`ArrayList`来存储`UserVO`对象,这些对象将用于构建最终的分页视图对象。
|
|
|
+3. 构建查询条件:
|
|
|
+ - 使用`LambdaQueryWrapper`构建查询条件。这个查询包装器会根据传入的参数动态地添加查询条件。
|
|
|
+ - `like`方法用于添加模糊匹配条件,`eq`方法用于添加精确匹配条件。
|
|
|
+ - `StringUtils.hasText`检查传入的字符串参数是否非空,只有非空时才会添加相应的查询条件。
|
|
|
+4. 执行分页查询:
|
|
|
+ - 使用`page`方法执行分页查询,传入分页参数和查询条件包装器。
|
|
|
+ - `page`方法返回一个`Page<User>`对象,包含了查询结果和分页信息。
|
|
|
+5. 转换结果并设置ID:
|
|
|
+ - 遍历查询结果中的记录,将每个`User`对象转换为`UserVO`对象。
|
|
|
+ - 使用`BeanCopyUtil.copyBean`方法进行对象属性的复制。
|
|
|
+ - 将`UserVO`对象的ID设置为字符串形式,以满足视图对象的要求。
|
|
|
+6. 构建并返回分页视图对象:
|
|
|
+ - 创建`PageVO`对象,传入用户视图对象列表和总记录数。
|
|
|
+ - 返回`PageVO`对象,包含了分页的用户信息和分页信息。
|
|
|
+
|
|
|
+```java
|
|
|
+ @Override
|
|
|
+ public PageVO pageByUsernameAndUseraccount(Integer currentPage, Integer pageSize, String userName, String userAccount, String userRole) {
|
|
|
+ List<UserVO> userVOList = new ArrayList<>();
|
|
|
+ LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>()
|
|
|
+ .like(StringUtils.hasText(userName), User::getUserName, userName)
|
|
|
+ .eq(StringUtils.hasText(userAccount), User::getUserAccount, userAccount)
|
|
|
+ .eq(StringUtils.hasText(userRole), User::getUserRole, userRole);
|
|
|
+ Page<User> page = page(new Page<User>(currentPage,pageSize), lambdaQueryWrapper);
|
|
|
+ for (User user : page.getRecords()) {
|
|
|
+ UserVO userVO = BeanCopyUtil.copyBean(user, UserVO.class);
|
|
|
+ userVO.setId(user.getId().toString());
|
|
|
+ userVOList.add(userVO);
|
|
|
+ }
|
|
|
+ return new PageVO(userVOList,page.getTotal());
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+##### 添加用户(AddUser)
|
|
|
+
|
|
|
+- **接口**:`POST /user`
|
|
|
+- **功能**:添加新用户。
|
|
|
+- **参数**:用户信息(账号、密码、名称、头像、角色)。
|
|
|
+- **返回**:操作结果。
|
|
|
+
|
|
|
+UserController
|
|
|
+
|
|
|
+一个添加新用户的接口,只有具有“管理员”角色的用户才能访问。
|
|
|
+
|
|
|
+它接收用户信息,执行添加用户的操作,并返回操作成功的响应。
|
|
|
+
|
|
|
+```java
|
|
|
+ @PreAuthorize("@ps.hasRole('管理员')")
|
|
|
+ @PostMapping
|
|
|
+ @ResponseBody
|
|
|
+ public ResponseResult addUser(@RequestBody UserDTO userDTO){
|
|
|
+ userService.addUser(userDTO);
|
|
|
+ return ResponseResult.okResult();
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+UserServce
|
|
|
+
|
|
|
+实现了将用户DTO转换为用户实体、加密密码、生成唯一ID,并保存用户信息到数据库的流程
|
|
|
+
|
|
|
+1. `public void addUser(UserDTO userDTO)`:这是一个公共方法,没有返回值(`void`),参数是一个`UserDTO`对象。
|
|
|
+2. 复制属性:
|
|
|
+ - 使用`BeanCopyUtil.copyBean`方法将`UserDTO`对象的属性复制到一个新的`User`对象中。这个工具方法通常用于将DTO(Data Transfer Object)对象的属性映射到实体对象。
|
|
|
+3. 密码加密:
|
|
|
+ - 使用`passwordEncoder.encode`方法对用户密码进行加密处理。
|
|
|
+ - 将加密后的密码设置回`user`对象的`userPassword`属性。
|
|
|
+4. 生成ID:
|
|
|
+ - 使用`IdUtil.getSnowflake(1, 1).nextId()`方法生成一个唯一的ID(基于Snowflake算法)。
|
|
|
+ - 将生成的ID设置为`user`对象的`id`属性。
|
|
|
+5. 保存用户:
|
|
|
+ - 调用`save`方法将`user`对象保存到数据库中。这个方法可能是一个Repository层的方法,负责实际的数据库操作。
|
|
|
+
|
|
|
+```java
|
|
|
+ public void addUser(UserDTO userDTO) {
|
|
|
+ User user = BeanCopyUtil.copyBean(userDTO, User.class);
|
|
|
+ //密码加密
|
|
|
+ user.setUserPassword(passwordEncoder.encode(user.getUserPassword()));
|
|
|
+ //生成ID
|
|
|
+ long id = IdUtil.getSnowflake(1, 1).nextId();
|
|
|
+ user.setId(id);
|
|
|
+ save(user);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+##### 更新用户信息(UpdateUser)
|
|
|
+
|
|
|
+- **接口**:`PUT /user`
|
|
|
+- **功能**:更新用户信息。
|
|
|
+- **参数**:用户信息(ID、账号、密码、名称、头像、角色)。
|
|
|
+- **返回**:操作结果。
|
|
|
+
|
|
|
+UserController
|
|
|
+
|
|
|
+一个更新用户的接口,只有具有“管理员”角色的用户才能访问。
|
|
|
+
|
|
|
+它接收用户信息,执行更新用户的操作,并返回操作成功的响应
|
|
|
+
|
|
|
+```java
|
|
|
+ @PreAuthorize("@ps.hasRole('管理员')")
|
|
|
+ @PutMapping
|
|
|
+ @ResponseBody
|
|
|
+ public ResponseResult updateUser(@RequestBody UserDTO userDTO){
|
|
|
+ User user = BeanCopyUtil.copyBean(userDTO, User.class);
|
|
|
+ userService.updateById(user);
|
|
|
+ return ResponseResult.okResult();
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+##### 删除用户(DeleteUser)
|
|
|
+
|
|
|
+- **接口**:`DELETE /user/{id}`
|
|
|
+- **功能**:根据ID删除用户。
|
|
|
+- **返回**:操作结果。
|
|
|
+
|
|
|
+UserController
|
|
|
+
|
|
|
+一个删除用户的接口,只有具有“管理员”角色的用户才能访问。
|
|
|
+
|
|
|
+它接收用户ID列表,执行删除用户的操作,并返回操作成功的响应
|
|
|
+
|
|
|
+```java
|
|
|
+ @PreAuthorize("@ps.hasRole('管理员')")
|
|
|
+ @DeleteMapping("/{ids}")
|
|
|
+ @ResponseBody
|
|
|
+ public ResponseResult deleteUserById(@PathVariable List<Long> ids){
|
|
|
+ userService.removeByIds(ids);
|
|
|
+ return ResponseResult.okResult();
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+完整的UserController
|
|
|
+
|
|
|
+```java
|
|
|
+package space.anyi.BI.controller;
|
|
|
+
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+import space.anyi.BI.entity.LoginUserDetails;
|
|
|
+import space.anyi.BI.entity.ResponseResult;
|
|
|
+import space.anyi.BI.entity.User;
|
|
|
+import space.anyi.BI.entity.dto.UserDTO;
|
|
|
+import space.anyi.BI.entity.vo.PageVO;
|
|
|
+import space.anyi.BI.entity.vo.UserVO;
|
|
|
+import space.anyi.BI.service.UserService;
|
|
|
+import space.anyi.BI.util.BeanCopyUtil;
|
|
|
+import space.anyi.BI.util.SecurityUtils;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Objects;
|
|
|
+
|
|
|
+@RequestMapping("/user")
|
|
|
+@RestController
|
|
|
+public class UserController {
|
|
|
+ private final static Logger log = LoggerFactory.getLogger(UserController.class);
|
|
|
+ @Resource
|
|
|
+ private UserService userService;
|
|
|
+
|
|
|
+ @PostMapping("/login")
|
|
|
+ public ResponseResult login(@RequestBody User user){
|
|
|
+ return userService.login(user);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/register")
|
|
|
+ public ResponseResult resister(@RequestBody User user){
|
|
|
+ return userService.register(user);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PreAuthorize("@ps.hasRole('用户')")
|
|
|
+ @GetMapping("/getUserInfo")
|
|
|
+ public ResponseResult getUserInfo(){
|
|
|
+ LoginUserDetails loginUser = SecurityUtils.getLoginUser();
|
|
|
+ if (Objects.isNull(loginUser)){
|
|
|
+ return ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.NEED_LOGIN);
|
|
|
+ }
|
|
|
+ User user = loginUser.getUser();
|
|
|
+ user.setUserPassword("");
|
|
|
+ System.out.println("user = " + user);
|
|
|
+ return ResponseResult.okResult(user);
|
|
|
+ }
|
|
|
+ @PreAuthorize("@ps.hasRole('管理员')")
|
|
|
+ @GetMapping("/page")
|
|
|
+ @ResponseBody
|
|
|
+ public ResponseResult page(Integer currentPage,Integer pageSize,String userName,String userAccount,String userRole){
|
|
|
+ log.info("currentPage:{},pageSize:{},userName:{},userAccount:{}",currentPage,pageSize,userName,userAccount);
|
|
|
+ PageVO page = userService.pageByUsernameAndUseraccount(currentPage, pageSize, userName, userAccount,userRole);
|
|
|
+ return ResponseResult.okResult(page);
|
|
|
+ }
|
|
|
+ @PreAuthorize("@ps.hasRole('用户')")
|
|
|
+ @GetMapping("/{id}")
|
|
|
+ @ResponseBody
|
|
|
+ public ResponseResult getUserById(@PathVariable Long id){
|
|
|
+ User user = userService.getById(id);
|
|
|
+ UserVO userVO = BeanCopyUtil.copyBean(user, UserVO.class);
|
|
|
+ userVO.setId(user.getId().toString());
|
|
|
+ return ResponseResult.okResult(userVO);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PreAuthorize("@ps.hasRole('管理员')")
|
|
|
+ @DeleteMapping("/{ids}")
|
|
|
+ @ResponseBody
|
|
|
+ public ResponseResult deleteUserById(@PathVariable List<Long> ids){
|
|
|
+ userService.removeByIds(ids);
|
|
|
+ return ResponseResult.okResult();
|
|
|
+ }
|
|
|
+
|
|
|
+ @PreAuthorize("@ps.hasRole('管理员')")
|
|
|
+ @PutMapping
|
|
|
+ @ResponseBody
|
|
|
+ public ResponseResult updateUser(@RequestBody UserDTO userDTO){
|
|
|
+ User user = BeanCopyUtil.copyBean(userDTO, User.class);
|
|
|
+ userService.updateById(user);
|
|
|
+ return ResponseResult.okResult();
|
|
|
+ }
|
|
|
+
|
|
|
+ @PreAuthorize("@ps.hasRole('管理员')")
|
|
|
+ @PostMapping
|
|
|
+ @ResponseBody
|
|
|
+ public ResponseResult addUser(@RequestBody UserDTO userDTO){
|
|
|
+ userService.addUser(userDTO);
|
|
|
+ return ResponseResult.okResult();
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+完整的UserServce
|
|
|
+
|
|
|
+```java
|
|
|
+package space.anyi.BI.service.impl;
|
|
|
+
|
|
|
+import cn.hutool.core.util.IdUtil;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
+import org.springframework.security.authentication.AuthenticationManager;
|
|
|
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
|
+import org.springframework.security.core.Authentication;
|
|
|
+import org.springframework.security.crypto.password.PasswordEncoder;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+import space.anyi.BI.constant.SystemConstants;
|
|
|
+import space.anyi.BI.entity.LoginUserDetails;
|
|
|
+import space.anyi.BI.entity.ResponseResult;
|
|
|
+import space.anyi.BI.entity.User;
|
|
|
+import space.anyi.BI.entity.dto.UserDTO;
|
|
|
+import space.anyi.BI.entity.vo.PageVO;
|
|
|
+import space.anyi.BI.entity.vo.UserInfo;
|
|
|
+import space.anyi.BI.entity.vo.UserVO;
|
|
|
+import space.anyi.BI.exception.SystemException;
|
|
|
+import space.anyi.BI.service.UserService;
|
|
|
+import space.anyi.BI.mapper.UserMapper;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import space.anyi.BI.util.BeanCopyUtil;
|
|
|
+import space.anyi.BI.util.JwtUtil;
|
|
|
+import space.anyi.BI.util.RedisCache;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
|
|
|
+ @Resource
|
|
|
+ private AuthenticationManager authenticationManager;
|
|
|
+ @Resource
|
|
|
+ private RedisCache redisCache;
|
|
|
+ @Resource
|
|
|
+ private PasswordEncoder passwordEncoder;
|
|
|
+ @Override
|
|
|
+ public ResponseResult login(User user) {
|
|
|
+ //认证
|
|
|
+ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserAccount(), user.getUserPassword());
|
|
|
+ Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
|
|
+ if (Objects.isNull(authentication)){
|
|
|
+ throw new SystemException(ResponseResult.AppHttpCodeEnum.LOGIN_ERROR);
|
|
|
+ }
|
|
|
+ //生成token
|
|
|
+ LoginUserDetails principal = (LoginUserDetails) authentication.getPrincipal();
|
|
|
+ Long id = principal.getUser().getId();
|
|
|
+ String jwt = JwtUtil.createJWT(id.toString());
|
|
|
+ //todo:权限
|
|
|
+ //存入redis
|
|
|
+ redisCache.setCacheObject(SystemConstants.LOGIN_USER_REDIS_KEY+id,principal);
|
|
|
+ //返回结果
|
|
|
+ Map<String,Object> map = new HashMap<>();
|
|
|
+ UserInfo userInfo = new UserInfo(principal.getUser().getUserAccount(),principal.getUser().getUserName(),principal.getUser().getUserAvatar(),principal.getUser().getUserRole());
|
|
|
+ map.put("token",jwt);
|
|
|
+ map.put("userInfo",userInfo);
|
|
|
+ return ResponseResult.okResult(map);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void addUser(UserDTO userDTO) {
|
|
|
+ User user = BeanCopyUtil.copyBean(userDTO, User.class);
|
|
|
+ //密码加密
|
|
|
+ user.setUserPassword(passwordEncoder.encode(user.getUserPassword()));
|
|
|
+ //生成ID
|
|
|
+ long id = IdUtil.getSnowflake(1, 1).nextId();
|
|
|
+ user.setId(id);
|
|
|
+ save(user);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public PageVO pageByUsernameAndUseraccount(Integer currentPage, Integer pageSize, String userName, String userAccount, String userRole) {
|
|
|
+ List<UserVO> userVOList = new ArrayList<>();
|
|
|
+ LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>()
|
|
|
+ .like(StringUtils.hasText(userName), User::getUserName, userName)
|
|
|
+ .eq(StringUtils.hasText(userAccount), User::getUserAccount, userAccount)
|
|
|
+ .eq(StringUtils.hasText(userRole), User::getUserRole, userRole);
|
|
|
+ Page<User> page = page(new Page<User>(currentPage,pageSize), lambdaQueryWrapper);
|
|
|
+ for (User user : page.getRecords()) {
|
|
|
+ UserVO userVO = BeanCopyUtil.copyBean(user, UserVO.class);
|
|
|
+ userVO.setId(user.getId().toString());
|
|
|
+ userVOList.add(userVO);
|
|
|
+ }
|
|
|
+ return new PageVO(userVOList,page.getTotal());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ResponseResult register(User user) {
|
|
|
+ //用户账号是否存在
|
|
|
+ if (userAccountExist(user.getUserAccount())){
|
|
|
+ throw new SystemException(ResponseResult.AppHttpCodeEnum.USERNAME_EXIST);
|
|
|
+ }
|
|
|
+ //密码加密
|
|
|
+ user.setUserPassword(passwordEncoder.encode(user.getUserPassword()));
|
|
|
+ //生成ID
|
|
|
+ long id = IdUtil.getSnowflake(1, 1).nextId();
|
|
|
+ user.setId(id);
|
|
|
+ //存入数据库
|
|
|
+ save(user);
|
|
|
+ return ResponseResult.okResult();
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 判断用户账号是否已被注册
|
|
|
+ * @param userAccount
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private boolean userAccountExist(String userAccount) {
|
|
|
+ LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(User::getUserAccount,userAccount);
|
|
|
+ if (count(queryWrapper)>0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+#### 权限控制
|
|
|
+
|
|
|
+用户管理模块中的所有操作都需要进行权限检查,确保只有具备相应权限的用户才能执行操作。例如,只有管理员可以添加、更新和删除用户。
|
|
|
+
|
|
|
+##### 认证
|
|
|
+
|
|
|
+- JWTAuthenticationTokenFilter认证过滤器
|
|
|
+ - 这个过滤器的作用是验证传入的JWT,如果验证成功,则将用户信息放入Spring Security的上下文中,允许请求继续处理。
|
|
|
+ - 如果验证失败,则返回错误响应,阻止请求继续
|
|
|
+
|
|
|
+```java
|
|
|
+package space.anyi.BI.filter;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import io.jsonwebtoken.Claims;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
|
+import org.springframework.security.core.Authentication;
|
|
|
+import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+import org.springframework.web.filter.OncePerRequestFilter;
|
|
|
+import space.anyi.BI.BIApplication;
|
|
|
+import space.anyi.BI.constant.SystemConstants;
|
|
|
+import space.anyi.BI.entity.LoginUserDetails;
|
|
|
+import space.anyi.BI.entity.ResponseResult;
|
|
|
+import space.anyi.BI.util.JwtUtil;
|
|
|
+import space.anyi.BI.util.RedisCache;
|
|
|
+import space.anyi.BI.util.WebUtils;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import javax.servlet.FilterChain;
|
|
|
+import javax.servlet.ServletException;
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import javax.servlet.http.HttpServletResponse;
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.Objects;
|
|
|
+
|
|
|
+@Component
|
|
|
+public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
|
|
|
+ private final static Logger log = LoggerFactory.getLogger(BIApplication.class);
|
|
|
+ @Resource
|
|
|
+ private RedisCache redisCache;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
|
|
|
+ //获取token
|
|
|
+ String uri = httpServletRequest.getRequestURI();
|
|
|
+ String token = httpServletRequest.getHeader("token");
|
|
|
+ log.info("token={},\n请求的uri={}",token,uri);
|
|
|
+ if (!StringUtils.hasText(token)) {
|
|
|
+ filterChain.doFilter(httpServletRequest, httpServletResponse);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ //解析token
|
|
|
+ Claims jwt;
|
|
|
+ try {
|
|
|
+ jwt = JwtUtil.parseJWT(token);
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ //响应前端,需要登陆
|
|
|
+ ResponseResult errorResult = ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.NEED_LOGIN);
|
|
|
+ WebUtils.renderString(httpServletResponse, JSON.toJSONString(errorResult));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ //从redis中获取用户信息
|
|
|
+ LoginUserDetails loginUserDetails = redisCache.getCacheObject(SystemConstants.LOGIN_USER_REDIS_KEY +jwt.getSubject());
|
|
|
+
|
|
|
+ //已退出登陆或登陆已过期
|
|
|
+ if(Objects.isNull(loginUserDetails)){
|
|
|
+ //响应前端,需要登陆
|
|
|
+ ResponseResult errorResult = ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.NEED_LOGIN);
|
|
|
+ WebUtils.renderString(httpServletResponse, JSON.toJSONString(errorResult));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ //存入SecurityContextHolder
|
|
|
+ Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginUserDetails,null,null);
|
|
|
+ SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
|
|
+ //放行
|
|
|
+ filterChain.doFilter(httpServletRequest,httpServletResponse);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- 鉴权
|
|
|
+
|
|
|
+```java
|
|
|
+package space.anyi.BI.service.impl;
|
|
|
+
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import space.anyi.BI.service.PermissionService;
|
|
|
+import space.anyi.BI.util.SecurityUtils;
|
|
|
+
|
|
|
+@Service("ps")
|
|
|
+public class PermissionServiceImpl implements PermissionService {
|
|
|
+ @Override
|
|
|
+ public boolean hasRole(String role) {
|
|
|
+ String userRole = SecurityUtils.getLoginUser().getUser().getUserRole();
|
|
|
+ if ("管理员".equals(userRole))return true;
|
|
|
+ if (userRole.equals(role)){
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- 安全配置
|
|
|
+
|
|
|
+```java
|
|
|
+package space.anyi.BI.config;
|
|
|
+
|
|
|
+import org.springframework.context.annotation.Bean;
|
|
|
+import org.springframework.context.annotation.Configuration;
|
|
|
+import org.springframework.http.HttpMethod;
|
|
|
+import org.springframework.security.authentication.AuthenticationManager;
|
|
|
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
|
|
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
|
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
|
|
+import org.springframework.security.config.http.SessionCreationPolicy;
|
|
|
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
|
+import org.springframework.security.crypto.password.PasswordEncoder;
|
|
|
+import org.springframework.security.web.AuthenticationEntryPoint;
|
|
|
+import org.springframework.security.web.access.AccessDeniedHandler;
|
|
|
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
|
|
+import space.anyi.BI.filter.JWTAuthenticationTokenFilter;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+
|
|
|
+@EnableGlobalMethodSecurity(prePostEnabled = true)
|
|
|
+@Configuration
|
|
|
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
+ @Resource
|
|
|
+ private JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
|
|
|
+ @Resource
|
|
|
+ private AccessDeniedHandler accessDeniedHandler;
|
|
|
+ @Resource
|
|
|
+ private AuthenticationEntryPoint authenticationEntryPoint;
|
|
|
+ /**
|
|
|
+ * 密码校验方式
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ @Bean
|
|
|
+ public PasswordEncoder passwordEncoder(){
|
|
|
+ return new BCryptPasswordEncoder();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 安全配置
|
|
|
+ * @param http
|
|
|
+ * @throws Exception
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ protected void configure(HttpSecurity http) throws Exception {
|
|
|
+ http
|
|
|
+ //关闭csrf
|
|
|
+ .csrf().disable()
|
|
|
+ //不通过Session获取SecurityContext
|
|
|
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
|
+ .and()
|
|
|
+ .authorizeRequests()
|
|
|
+ // 对于登录接口 允许匿名访问
|
|
|
+ .antMatchers(HttpMethod.POST,"/user/login","/user/register").anonymous()
|
|
|
+ .antMatchers(HttpMethod.GET,"/swagger/**","/v2/**","/v2/api-docs-ext/**","/swagger-resources/**").anonymous()
|
|
|
+ //放行对静态资源的访问
|
|
|
+ .antMatchers(HttpMethod.GET,"/","/*.html","/**/*.css","/**/*.js","/img/**","/fonts/**").permitAll()
|
|
|
+ .antMatchers(HttpMethod.GET,"/user/getUserInfo").permitAll()
|
|
|
+ .antMatchers(HttpMethod.POST,"/user/logout","/user/updatePassword").authenticated()
|
|
|
+ .antMatchers(HttpMethod.POST,"/uploadImage").permitAll()
|
|
|
+ // 除上面外的所有请求全部需要认证访问
|
|
|
+ .anyRequest().authenticated();
|
|
|
+
|
|
|
+ //关闭Security默认的退出接口
|
|
|
+ http.logout().disable();
|
|
|
+ http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
|
|
|
+
|
|
|
+ http.exceptionHandling()
|
|
|
+ .accessDeniedHandler(accessDeniedHandler)
|
|
|
+ .authenticationEntryPoint(authenticationEntryPoint);
|
|
|
+ //允许跨域
|
|
|
+ http.cors();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Bean
|
|
|
+ public AuthenticationManager authenticationManagerBean() throws Exception {
|
|
|
+ return super.authenticationManagerBean();
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+#### 前端页面展示
|
|
|
+
|
|
|
+##### 登陆和注册
|
|
|
+
|
|
|
+![image-20241215210327845](http://tuchuang.anyi.space/imgs/image-20241215210327845.png)
|
|
|
+
|
|
|
+![image-20241215210432964](http://tuchuang.anyi.space/imgs/image-20241215210432964.png)
|
|
|
+
|
|
|
+- **功能**:用户的登陆和注册
|
|
|
+
|
|
|
+- 元素
|
|
|
+
|
|
|
+ - 登陆表单
|
|
|
+
|
|
|
+ ### `<template>` 部分
|
|
|
+
|
|
|
+ 1. `<div id="b-container" class="container b-container">`:定义了一个包含登录表单的容器。
|
|
|
+ 2. `<form id="b-form" class="form" method="POST" action="">`:定义了一个登录表单,`method="POST"`表示表单提交时将使用POST请求,`action=""`属性为空,因为表单提交将通过JavaScript处理。
|
|
|
+ 3. `<h2 class="form_title title">Sign in to Website</h2>` 和 `<span class="form__span">or use your email account</span>`:提供了登录表单的标题和副标题。
|
|
|
+ 4. `<input v-model="loginFrom.userAccount" class="form__input" type="text" placeholder="用户名" />` 和 `<input v-model.lazy="loginFrom.userPassword" class="form__input" type="password" placeholder="密码" />`:定义了两个输入框,分别用于输入用户名和密码。`v-model`用于创建数据的双向绑定,`v-model.lazy`用于在`input`事件后更新数据。
|
|
|
+ 5. `<a class="form__link">Forgot your password?</a>`:提供了一个忘记密码的链接。
|
|
|
+ 6. `<button class="form__button button submit" @click.prevent="loginHandler()">SIGN IN</button>`:定义了一个提交按钮,`@click.prevent`阻止了表单的默认提交行为,并调用了`loginHandler`方法。
|
|
|
+
|
|
|
+ ### `<script setup>` 部分
|
|
|
+
|
|
|
+ 1. 导入必要的依赖:`ref`, `watch`从`vue`,`useMainStore`从`@/stores/message`,`storeToRefs`从`pinia`,`ElMessage`从`element-plus`,`login`从`@/common/api/user/index`,以及`clearTokenAndUserInfo`, `saveUserInfo`, `setToken`从`../common/utils/auth.js`。
|
|
|
+ 2. 创建`mainStore`实例,并使用`storeToRefs`来响应式地监听`showSignup`状态的变化。
|
|
|
+ 3. 使用`watch`来监听`showSignup`的变化,并根据其值切换容器的CSS类。
|
|
|
+ 4. 定义`loginFrom`响应式对象,用于存储表单数据。
|
|
|
+ 5. 定义`loginHandler`方法,该方法执行登录操作:
|
|
|
+ - 调用`clearTokenAndUserInfo`清除旧的认证信息。
|
|
|
+ - 调用`login` API方法,传入`loginFrom.value`(包含用户名和密码)。
|
|
|
+ - 如果登录成功(`res.code == 200`),则设置token,保存用户信息,并跳转到首页。
|
|
|
+ - 如果登录失败,则显示错误消息。
|
|
|
+ - 如果有错误发生,则在控制台打印错误并显示错误消息。
|
|
|
+ 6. `router.push`用于在登录成功后导航到首页。
|
|
|
+
|
|
|
+ - ```vue
|
|
|
+ <template>
|
|
|
+ <div id="b-container" class="container b-container">
|
|
|
+ <form id="b-form" class="form" method="POST" action="">
|
|
|
+ <h2 class="form_title title">Sign in to Website</h2>
|
|
|
+ <span class="form__span">or use your email account</span>
|
|
|
+ <input v-model="loginFrom.userAccount" class="form__input" type="text" placeholder="用户名" />
|
|
|
+ <input v-model.lazy="loginFrom.userPassword" class="form__input" type="password" placeholder="密码" />
|
|
|
+ <a class="form__link">Forgot your password?</a>
|
|
|
+ <button class="form__button button submit" @click.prevent="loginHandler()">SIGN IN</button>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <script setup >
|
|
|
+ import {ref, watch} from 'vue'
|
|
|
+ import { useMainStore } from '@/stores/message'
|
|
|
+ import {storeToRefs} from "pinia";
|
|
|
+ import { ElMessage } from 'element-plus'
|
|
|
+ import {login} from "@/common/api/user/index";
|
|
|
+ import {clearTokenAndUserInfo, saveUserInfo, setToken} from "../common/utils/auth.js";
|
|
|
+ import router from "../router/index.js";
|
|
|
+
|
|
|
+ const mainStore = useMainStore()
|
|
|
+ const { showSignup } = storeToRefs(mainStore)
|
|
|
+ watch(showSignup, () => {
|
|
|
+ const bContainer = document.querySelector('#b-container')
|
|
|
+ bContainer.classList.toggle('is-txl')
|
|
|
+ bContainer.classList.toggle('is-z200')
|
|
|
+ })
|
|
|
+
|
|
|
+ const loginFrom = ref({
|
|
|
+ userAccount: '',
|
|
|
+ userPassword: '',
|
|
|
+ })
|
|
|
+ let loginHandler = () =>{
|
|
|
+ clearTokenAndUserInfo();
|
|
|
+ login(loginFrom.value).then(res =>{
|
|
|
+ if (res.code == 200){
|
|
|
+ setToken(res.data.token);
|
|
|
+ saveUserInfo(res.data.userInfo)
|
|
|
+
|
|
|
+ router.push({
|
|
|
+ name:'index',
|
|
|
+ path:'/index'
|
|
|
+ });
|
|
|
+ ElMessage({
|
|
|
+ message: '登陆成功',
|
|
|
+ type:'success'
|
|
|
+ })
|
|
|
+ }else {
|
|
|
+ ElMessage({
|
|
|
+ message: res.msg,
|
|
|
+ type:'error'
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ }).catch(err =>{
|
|
|
+ console.log(err)
|
|
|
+ ElMessage({
|
|
|
+ message: err,
|
|
|
+ type:'error'
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ - 注册表单
|
|
|
+
|
|
|
+ - ```vue
|
|
|
+ <template>
|
|
|
+ <div id="a-container" class="container a-container">
|
|
|
+ <form id="a-form" class="form" method="POST" action="">
|
|
|
+ <h2 class="form_title title">Create Account</h2>
|
|
|
+ <span class="form__span">or use email for registration</span>
|
|
|
+ <input class="form__input" v-model="user.userAccount" type="text" placeholder="Account" />
|
|
|
+ <input class="form__input" v-model.lazy="user.userPassword" type="password" placeholder="Password" />
|
|
|
+ <button class="form__button button submit" @click.prevent="registerHandler()">SIGN UP</button>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <script setup >
|
|
|
+ import { useMainStore } from '@/stores/message'
|
|
|
+ import {ref, watch} from "vue";
|
|
|
+ import {storeToRefs} from "pinia";
|
|
|
+ import {ElMessage} from "element-plus";
|
|
|
+ import {userRegister} from '@/common/api/user/index'
|
|
|
+ const mainStore = useMainStore()
|
|
|
+ const { showSignup } = storeToRefs(mainStore)
|
|
|
+ watch(showSignup, () => {
|
|
|
+ const aContainer = document.querySelector('#a-container')
|
|
|
+ aContainer.classList.toggle('is-txl')
|
|
|
+ })
|
|
|
+ const user = ref({
|
|
|
+ userName:'用户'+Date.now(),
|
|
|
+ userAccount: '',
|
|
|
+ userPassword: '',
|
|
|
+ })
|
|
|
+ function registerHandler(){
|
|
|
+ userRegister(user.value).then(res =>{
|
|
|
+ if (res.code == 200){
|
|
|
+ ElMessage({
|
|
|
+ message: '注册成功',
|
|
|
+ type:'success'
|
|
|
+ })
|
|
|
+ }else {
|
|
|
+ ElMessage({
|
|
|
+ message: res.msg,
|
|
|
+ type:'error'
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ }).catch(err =>{
|
|
|
+ console.log(err)
|
|
|
+ ElMessage({
|
|
|
+ message: err,
|
|
|
+ type:'error'
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+##### 用户列表页面
|
|
|
+
|
|
|
+![image-20241215210510386](http://tuchuang.anyi.space/imgs/image-20241215210510386.png)
|
|
|
+
|
|
|
+- **功能**:展示用户列表,提供搜索、分页功能。
|
|
|
+- **元素**:
|
|
|
+ - 用户信息表格。
|
|
|
+
|
|
|
+ - ```vue
|
|
|
+ <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
|
|
|
+ <el-table-column type="selection" width="55" align="center" />
|
|
|
+ <el-table-column label="用户ID" align="center" prop="id" />
|
|
|
+ <el-table-column label="用户账号" align="center" prop="userAccount" />
|
|
|
+ <el-table-column label="用户名称" align="center" prop="userName" />
|
|
|
+ <el-table-column label="用户头像" align="center" prop="userAvatar" />
|
|
|
+ <el-table-column label="用户角色" align="center" prop="userRole" />
|
|
|
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="mini"
|
|
|
+ @click="handleUpdate(scope.row)"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <el-icon-edit/>
|
|
|
+ </template>
|
|
|
+ 修改</el-button>
|
|
|
+
|
|
|
+ <el-popconfirm
|
|
|
+ confirm-button-text="确定"
|
|
|
+ cancel-button-text="取消"
|
|
|
+ icon-color="#626AEF"
|
|
|
+ title="确定删除这个用户吗?"
|
|
|
+ @confirm="handleDelete(scope.row)"
|
|
|
+ @cancel="cancelEvent"
|
|
|
+ >
|
|
|
+ <InfoFilled/>
|
|
|
+ <template #reference>
|
|
|
+ <el-button type="danger">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-popconfirm>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+ - 新增、编辑、删除按钮。
|
|
|
+
|
|
|
+ - ```vue
|
|
|
+ <el-row :gutter="10" class="mb8">
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ plain
|
|
|
+ size="mini"
|
|
|
+ @click="handleAdd"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ 新增
|
|
|
+ </template>
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ plain
|
|
|
+ size="mini"
|
|
|
+ :disabled="single"
|
|
|
+ @click="handleUpdate"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <el-icon><Edit /></el-icon>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ 修改
|
|
|
+ </template>
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ plain
|
|
|
+ size="mini"
|
|
|
+ :disabled="multiple"
|
|
|
+ @click="handleDelete"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <el-icon><Remove /></el-icon>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ 删除
|
|
|
+ </template>
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
|
|
+ </el-row>
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+ - 用户条件搜索,用户角色筛选。
|
|
|
+
|
|
|
+ - ```vue
|
|
|
+ <div class="app-container">
|
|
|
+ <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
|
|
+ <el-form-item label="用户账号" prop="userAccount">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.userAccount"
|
|
|
+ placeholder="请输入用户账号"
|
|
|
+ clearable
|
|
|
+ @keyup.enter.native="handleQuery"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户名称" prop="userName">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.userName"
|
|
|
+ placeholder="请输入用户名称"
|
|
|
+ clearable
|
|
|
+ @keyup.enter.native="handleQuery"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户角色" prop="userRole" style="width: 200px">
|
|
|
+ <el-select v-model="queryParams.userRole" placeholder="请选择用户角色" clearable>
|
|
|
+ <el-option :value="'用户'" :label="'用户'" :key="'用户'"/>
|
|
|
+ <el-option :value="'管理员'" :label="'管理员' " :key="'管理员'"/>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" size="mini" @click="handleQuery">
|
|
|
+ <template #icon>
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ 搜索
|
|
|
+ </template>
|
|
|
+ </el-button>
|
|
|
+ <el-button size="mini" @click="resetQuery">
|
|
|
+ <template #icon>
|
|
|
+ <el-icon><RefreshRight /></el-icon>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ 重置
|
|
|
+ </template>
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ ```
|
|
|
+
|
|
|
+##### 用户详情页面
|
|
|
+
|
|
|
+![image-20241215210550119](http://tuchuang.anyi.space/imgs/image-20241215210550119.png)
|
|
|
+
|
|
|
+- **功能**:展示和编辑用户详细信息。
|
|
|
+- **元素**:
|
|
|
+ - 用户账号、密码、名称、头像、角色的输入框。
|
|
|
+
|
|
|
+ - ```vue
|
|
|
+ <el-dialog :title="title" :model-value="open" width="500px" append-to-body>
|
|
|
+ <el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
|
|
+ <el-form-item label="用户账号" prop="userAccount">
|
|
|
+ <el-input v-model="form.userAccount" placeholder="请输入用户账号" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户密码" prop="userPassword">
|
|
|
+ <el-input v-model="form.userPassword" placeholder="请输入用户密码" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户名称" prop="userName">
|
|
|
+ <el-input v-model="form.userName" placeholder="请输入用户名称" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户头像" prop="userAvatar">
|
|
|
+ <el-input v-model="form.userAvatar" type="textarea" placeholder="请输入内容" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户角色" prop="userRole">
|
|
|
+ <!--<el-input v-model="form.userRole" placeholder="请输入用户角色" />-->
|
|
|
+ <el-select v-model="form.userRole" placeholder="请选择用户角色">
|
|
|
+ <el-option :value="'用户'" :label="'用户'"/>
|
|
|
+ <el-option :value="'管理员'" :label="'管理员'"/>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer" class="dialog-footer">
|
|
|
+ <el-button type="primary" @click="submitForm">确 定</el-button>
|
|
|
+ <el-button @click="cancel">取 消</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+ - 保存和取消按钮。
|
|
|
+
|
|
|
+ ##### 完整页面代码
|
|
|
+
|
|
|
+ ```vue
|
|
|
+ <template>
|
|
|
+ <div class="app-container">
|
|
|
+ <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
|
|
+ <el-form-item label="用户账号" prop="userAccount">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.userAccount"
|
|
|
+ placeholder="请输入用户账号"
|
|
|
+ clearable
|
|
|
+ @keyup.enter.native="handleQuery"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户名称" prop="userName">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.userName"
|
|
|
+ placeholder="请输入用户名称"
|
|
|
+ clearable
|
|
|
+ @keyup.enter.native="handleQuery"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户角色" prop="userRole" style="width: 200px">
|
|
|
+ <!--<el-input-->
|
|
|
+ <!-- v-model="queryParams.userRole"-->
|
|
|
+ <!-- placeholder="请输入用户角色"-->
|
|
|
+ <!-- clearable-->
|
|
|
+ <!-- @keyup.enter.native="handleQuery"-->
|
|
|
+ <!--/>-->
|
|
|
+ <el-select v-model="queryParams.userRole" placeholder="请选择用户角色" clearable>
|
|
|
+ <el-option :value="'用户'" :label="'用户'" :key="'用户'"/>
|
|
|
+ <el-option :value="'管理员'" :label="'管理员' " :key="'管理员'"/>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" size="mini" @click="handleQuery">
|
|
|
+ <template #icon>
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ 搜索
|
|
|
+ </template>
|
|
|
+ </el-button>
|
|
|
+ <el-button size="mini" @click="resetQuery">
|
|
|
+ <template #icon>
|
|
|
+ <el-icon><RefreshRight /></el-icon>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ 重置
|
|
|
+ </template>
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <el-row :gutter="10" class="mb8">
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ plain
|
|
|
+ size="mini"
|
|
|
+ @click="handleAdd"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ 新增
|
|
|
+ </template>
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ plain
|
|
|
+ size="mini"
|
|
|
+ :disabled="single"
|
|
|
+ @click="handleUpdate"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <el-icon><Edit /></el-icon>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ 修改
|
|
|
+ </template>
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ plain
|
|
|
+ size="mini"
|
|
|
+ :disabled="multiple"
|
|
|
+ @click="handleDelete"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <el-icon><Remove /></el-icon>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ 删除
|
|
|
+ </template>
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
|
|
|
+ <el-table-column type="selection" width="55" align="center" />
|
|
|
+ <el-table-column label="用户ID" align="center" prop="id" />
|
|
|
+ <el-table-column label="用户账号" align="center" prop="userAccount" />
|
|
|
+ <el-table-column label="用户名称" align="center" prop="userName" />
|
|
|
+ <el-table-column label="用户头像" align="center" prop="userAvatar" />
|
|
|
+ <el-table-column label="用户角色" align="center" prop="userRole" />
|
|
|
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="mini"
|
|
|
+ @click="handleUpdate(scope.row)"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <el-icon-edit/>
|
|
|
+ </template>
|
|
|
+ 修改</el-button>
|
|
|
+
|
|
|
+ <el-popconfirm
|
|
|
+ confirm-button-text="确定"
|
|
|
+ cancel-button-text="取消"
|
|
|
+ icon-color="#626AEF"
|
|
|
+ title="确定删除这个用户吗?"
|
|
|
+ @confirm="handleDelete(scope.row)"
|
|
|
+ @cancel="cancelEvent"
|
|
|
+ >
|
|
|
+ <InfoFilled/>
|
|
|
+ <template #reference>
|
|
|
+ <el-button type="danger">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-popconfirm>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <el-pagination
|
|
|
+ v-show="total>0"
|
|
|
+ v-model:current-page="queryParams.currentPage"
|
|
|
+ v-model:page-size="queryParams.pageSize"
|
|
|
+ :page-sizes="[10, 15, 30]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ :total="total"
|
|
|
+ @size-change="getList"
|
|
|
+ @current-change="getList"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 添加或修改用户对话框 -->
|
|
|
+ <el-dialog :title="title" :model-value="open" width="500px" append-to-body>
|
|
|
+ <el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
|
|
+ <el-form-item label="用户账号" prop="userAccount">
|
|
|
+ <el-input v-model="form.userAccount" placeholder="请输入用户账号" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户密码" prop="userPassword">
|
|
|
+ <el-input v-model="form.userPassword" placeholder="请输入用户密码" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户名称" prop="userName">
|
|
|
+ <el-input v-model="form.userName" placeholder="请输入用户名称" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户头像" prop="userAvatar">
|
|
|
+ <el-input v-model="form.userAvatar" type="textarea" placeholder="请输入内容" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="用户角色" prop="userRole">
|
|
|
+ <!--<el-input v-model="form.userRole" placeholder="请输入用户角色" />-->
|
|
|
+ <el-select v-model="form.userRole" placeholder="请选择用户角色">
|
|
|
+ <el-option :value="'用户'" :label="'用户'"/>
|
|
|
+ <el-option :value="'管理员'" :label="'管理员'"/>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer" class="dialog-footer">
|
|
|
+ <el-button type="primary" @click="submitForm">确 定</el-button>
|
|
|
+ <el-button @click="cancel">取 消</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ import { listUser, getUser, delUser, addUser, updateUser } from "@/common/api/user";
|
|
|
+ import {ElMessage} from "element-plus";
|
|
|
+
|
|
|
+ export default {
|
|
|
+ name: "User",
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ // 遮罩层
|
|
|
+ loading: true,
|
|
|
+ // 选中数组
|
|
|
+ ids: [],
|
|
|
+ // 非单个禁用
|
|
|
+ single: true,
|
|
|
+ // 非多个禁用
|
|
|
+ multiple: true,
|
|
|
+ // 显示搜索条件
|
|
|
+ showSearch: true,
|
|
|
+ // 总条数
|
|
|
+ total: 0,
|
|
|
+ // 用户表格数据
|
|
|
+ userList: [],
|
|
|
+ // 弹出层标题
|
|
|
+ title: "",
|
|
|
+ // 是否显示弹出层
|
|
|
+ open: false,
|
|
|
+ // 查询参数
|
|
|
+ queryParams: {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ userAccount: null,
|
|
|
+ userName: null,
|
|
|
+ userRole: null,
|
|
|
+ },
|
|
|
+ // 表单参数
|
|
|
+ form: {},
|
|
|
+ // 表单校验
|
|
|
+ rules: {
|
|
|
+ userAccount: [
|
|
|
+ { required: true, message: "用户账号不能为空", trigger: "blur" }
|
|
|
+ ],
|
|
|
+ userPassword: [
|
|
|
+ { required: true, message: "用户密码不能为空", trigger: "blur" }
|
|
|
+ ],
|
|
|
+ userName: [
|
|
|
+ { required: true, message: "用户名称不能为空", trigger: "blur" }
|
|
|
+ ],
|
|
|
+ userRole: [
|
|
|
+ { required: true, message: "用户角色不能为空", trigger: "blur" }
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ /** 查询用户列表 */
|
|
|
+ getList() {
|
|
|
+ this.loading = true;
|
|
|
+ listUser(this.queryParams).then(response => {
|
|
|
+ this.userList = response.data.records;
|
|
|
+ this.total = response.data.total;
|
|
|
+ this.loading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ // 取消按钮
|
|
|
+ cancel() {
|
|
|
+ this.open = false;
|
|
|
+ this.reset();
|
|
|
+ },
|
|
|
+ // 表单重置
|
|
|
+ reset() {
|
|
|
+ this.form = {
|
|
|
+ id: null,
|
|
|
+ userAccount: null,
|
|
|
+ userPassword: null,
|
|
|
+ userName: null,
|
|
|
+ userAvatar: null,
|
|
|
+ userRole: null,
|
|
|
+ createTime: null,
|
|
|
+ updateTime: null,
|
|
|
+ deleteFlag: null
|
|
|
+ };
|
|
|
+ // this.resetForm("form");
|
|
|
+ // this.$refs.form.resetFields();
|
|
|
+ // this.$refs['form'].resetFields();
|
|
|
+ },
|
|
|
+ /** 搜索按钮操作 */
|
|
|
+ handleQuery() {
|
|
|
+ this.queryParams.currentPage = 1;
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+ /** 重置按钮操作 */
|
|
|
+ resetQuery() {
|
|
|
+ // this.resetForm("queryForm");
|
|
|
+ // this.$refs['queryForm'].resetFields();
|
|
|
+ this.queryParams = {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ userAccount: null,
|
|
|
+ userName: null,
|
|
|
+ userRole: null,
|
|
|
+ }
|
|
|
+ this.handleQuery();
|
|
|
+ },
|
|
|
+ // 多选框选中数据
|
|
|
+ handleSelectionChange(selection) {
|
|
|
+ this.ids = selection.map(item => item.id)
|
|
|
+ this.single = selection.length!==1
|
|
|
+ this.multiple = !selection.length
|
|
|
+ },
|
|
|
+ /** 新增按钮操作 */
|
|
|
+ handleAdd() {
|
|
|
+ this.reset();
|
|
|
+ this.open = true;
|
|
|
+ this.title = "添加用户";
|
|
|
+ },
|
|
|
+ /** 修改按钮操作 */
|
|
|
+ handleUpdate(row) {
|
|
|
+ this.reset();
|
|
|
+ const id = row.id || this.ids
|
|
|
+ getUser(id).then(response => {
|
|
|
+ this.form = response.data;
|
|
|
+ this.open = true;
|
|
|
+ this.title = "修改用户";
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 提交按钮 */
|
|
|
+ submitForm() {
|
|
|
+ this.$refs["form"].validate(valid => {
|
|
|
+ if (valid) {
|
|
|
+ if (this.form.id != null) {
|
|
|
+ updateUser(this.form).then(response => {
|
|
|
+ // this.$modal.msgSuccess("修改成功");
|
|
|
+ ElMessage({
|
|
|
+ message: '修改成功',
|
|
|
+ type: 'success',
|
|
|
+ });
|
|
|
+ this.open = false;
|
|
|
+ this.getList();
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ addUser(this.form).then(response => {
|
|
|
+ // this.$modal.msgSuccess("新增成功");
|
|
|
+ ElMessage({
|
|
|
+ message: '新增成功',
|
|
|
+ type: 'success',
|
|
|
+ });
|
|
|
+ this.open = false;
|
|
|
+ this.getList();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 删除按钮操作 */
|
|
|
+ handleDelete(row) {
|
|
|
+ const ids = row.id || this.ids;
|
|
|
+ delUser(ids).then(res => {
|
|
|
+ if (res.code == 200){
|
|
|
+ ElMessage({
|
|
|
+ message: '删除成功',
|
|
|
+ type: 'success',
|
|
|
+ });
|
|
|
+ }else {
|
|
|
+ ElMessage({
|
|
|
+ message: res.msg,
|
|
|
+ type: 'error',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }).error(err => {
|
|
|
+ ElMessage({
|
|
|
+ message: '删除失败',
|
|
|
+ type: 'error',
|
|
|
+ });
|
|
|
+ })
|
|
|
+ this.$modal.confirm('是否确认删除用户编号为"' + ids + '"的数据项?').then(function() {
|
|
|
+ return delUser(ids);
|
|
|
+ }).then(() => {
|
|
|
+ this.getList();
|
|
|
+ this.$modal.msgSuccess("删除成功");
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+ }
|
|
|
+ };
|
|
|
+ </script>
|
|
|
+
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+用户管理模块提供了一套完整的用户信息管理功能,包括用户登录、注册、信息查看、编辑和删除。
|
|
|
+
|
|
|
+通过严格的权限控制,确保了系统的安全性。前端页面友好、易用,提供了良好的用户体验。
|
|
|
+
|
|
|
+### 4.2 数据分析
|
|
|
+
|
|
|
+#### 后端
|
|
|
+
|
|
|
+- 数据分析和图表生成模块提供了完整的数据处理和可视化功能。
|
|
|
+
|
|
|
+- 通过令牌桶限流和线程池优化,确保了接口的稳定性和高并发处理能力
|
|
|
+
|
|
|
+- **同步与异步**:用户可以通过ChartController上传数据文件并请求图表生成,支持同步和异步两种模式。
|
|
|
+
|
|
|
+ - Controller
|
|
|
+
|
|
|
+ - 一个生成图表的接口,只有具有“用户”角色的用户才能访问。
|
|
|
+
|
|
|
+ - 它处理文件上传,执行限流检查,验证文件类型和大小,然后根据是否异步生成图表,并返回相应的结果
|
|
|
+
|
|
|
+ - ```java
|
|
|
+ @PreAuthorize("@ps.hasRole('用户')")
|
|
|
+ @PostMapping("/generateChartByAI")
|
|
|
+ @ResponseBody
|
|
|
+ public ResponseResult generateChartByAI(ChartDTO chartDTO, MultipartFile file) throws IOException {
|
|
|
+ log.info("分析目标:{},图标名称:{},是否异步{}",chartDTO.getAnalysisTarget(),chartDTO.getName(),chartDTO.getIsAsynchronism());
|
|
|
+ //限流判断
|
|
|
+ if (!rRateLimiterHandler.accessAble("generateChartByAI_"+SecurityUtils.getUserId())){
|
|
|
+ return ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.RATE_LIMIT_ERROR);
|
|
|
+ }
|
|
|
+ if (file == null || file.isEmpty()) {
|
|
|
+ return ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.FILE_NOT_NULL);
|
|
|
+ }
|
|
|
+ //文件类判断
|
|
|
+ if (!file.getContentType().equals("application/vnd.ms-excel") && !file.getContentType().equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) {
|
|
|
+ return ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.FILE_TYPE_ERROR);
|
|
|
+ }
|
|
|
+ //文件大小判断
|
|
|
+ if (file.getSize()>1024*1024*2L) {
|
|
|
+ return ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.FILE_SIZE_ERROR);
|
|
|
+ }
|
|
|
+ if (chartDTO.getIsAsynchronism()){
|
|
|
+ long id = chartService.generateChartByAIAsyn(chartDTO,file);
|
|
|
+ return ResponseResult.okResult("任务提交成功,请稍后查看结果",String.valueOf(id));
|
|
|
+ }else {
|
|
|
+ ChartVO vo = chartService.generateChartByAI(chartDTO,file);
|
|
|
+ return ResponseResult.okResult(vo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ - **同步**
|
|
|
+
|
|
|
+ - ![image-20241215211955088](http://tuchuang.anyi.space/imgs/image-20241215211955088.png)
|
|
|
+
|
|
|
+ - 实现了通过AI生成图表的完整流程,包括读取和验证上传的数据、构建AI请求、发送请求、解析响应、数据可视化处理和更新数据库
|
|
|
+
|
|
|
+ 1. **初始化和数据准备**:
|
|
|
+ - 生成一个唯一的图表ID。
|
|
|
+ - 从`ChartDTO`复制数据到`Chart`实体对象,并设置必要的属性,如ID、用户ID和状态(初始状态为“等待中”)。
|
|
|
+ 2. **读取和转换文件**:
|
|
|
+ - 从上传的Excel文件中读取数据,并将其转换为CSV格式的字符串数据。
|
|
|
+ 3. **数据验证**:
|
|
|
+ - 检查CSV数据的长度,如果超过3000字符,则更新图表状态为“失败”,记录错误信息,并抛出异常。
|
|
|
+ 4. **保存初始图表状态**:
|
|
|
+ - 将CSV数据保存到图表实体的`chartData`属性中,并将图表实体保存到数据库。
|
|
|
+ 5. **构建AI请求**:
|
|
|
+ - 构建一个包含原始数据、分析目标和图表类型的请求消息,用于向AI发送请求。
|
|
|
+ 6. **发送AI请求**:
|
|
|
+ - 使用`AiUtil.doChat`方法发送构建好的请求数据到AI服务,并接收响应数据。
|
|
|
+ 7. **解析AI响应**:
|
|
|
+ - 解析AI返回的数据,提取图表生成的内容(通常是一个Echarts的option代码)和分析结论。
|
|
|
+ 8. **错误处理**:
|
|
|
+ - 如果AI返回的数据中没有包含预期的图表生成内容(即没有找到界定图表内容的```标记),则更新图表状态为“失败”,记录错误信息,并抛出异常。
|
|
|
+ 9. **更新图表实体**:
|
|
|
+ - 将提取的Echarts的option代码保存到图表实体的`generatedChartData`属性中。
|
|
|
+ - 将分析结论保存到图表实体的`analysisConclusion`属性中。
|
|
|
+ - 更新图表实体的状态为“成功”,并记录图表生成成功的信息。
|
|
|
+ 10. **保存最终图表状态**:
|
|
|
+ - 将更新后的图表实体保存到数据库。
|
|
|
+ 11. **返回结果**:
|
|
|
+ - 将图表实体转换为`ChartVO`对象,以便将生成的图表数据和结论返回给前端。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ - ```java
|
|
|
+ @Override
|
|
|
+ public ChartVO generateChartByAI(ChartDTO chartDTO, MultipartFile file) {
|
|
|
+ //读数据
|
|
|
+ String csvData = "";
|
|
|
+ long charId = IdUtil.getSnowflake(1, 1).nextId();
|
|
|
+ Chart chart = BeanCopyUtil.copyBean(chartDTO, Chart.class);
|
|
|
+ chart.setId(charId);
|
|
|
+ chart.setUserId(SecurityUtils.getUserId());
|
|
|
+ chart.setState("等待中");
|
|
|
+ try {
|
|
|
+ csvData = ExcelUtils.excel2csv(file.getInputStream());
|
|
|
+ log.info("上传的数据为:\n{}", csvData);
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ if (csvData.length()>3000){
|
|
|
+ chart.setState("失败");
|
|
|
+ chart.setExecuteMessage("数据量过大,请上传小于3000行的数据");
|
|
|
+ chart.setChartData("");
|
|
|
+ save(chart);
|
|
|
+ throw new SystemException(500, "数据量过大,请上传小于3000行的数据");
|
|
|
+ }
|
|
|
+ chart.setChartData(csvData);
|
|
|
+ save(chart);
|
|
|
+ StringBuilder message = new StringBuilder("原始数据:\n");
|
|
|
+ message.append(csvData);
|
|
|
+ message.append("分析目标:\n");
|
|
|
+ message.append(chartDTO.getAnalysisTarget());
|
|
|
+ message.append("\n.使用").append(chartDTO.getChartType()).append("进行可视化分析.\n");
|
|
|
+ chart.setState("生成中");
|
|
|
+ chart.setExecuteMessage("AI正在生成图表");
|
|
|
+ updateById(chart);
|
|
|
+ //配置prompt向AI发送请求
|
|
|
+ HttpRequestData requestData = AiUtil.createDefaultRequestData(message.toString());
|
|
|
+ HttpResponseData responseData = AiUtil.doChat(requestData);
|
|
|
+ //解析AI返回的数据
|
|
|
+ //ChartVO chartVO = BeanCopyUtil.copyBean(chartDTO, ChartVO.class);
|
|
|
+ String content = responseData.getChoices().get(0).getMessage().getContent();
|
|
|
+ log.info("AI返回的数据为:{}", content);
|
|
|
+ int index = content.indexOf("```");
|
|
|
+ int endIndex = content.lastIndexOf("```");
|
|
|
+ if (index == -1 || endIndex == -1){
|
|
|
+ chart.setState("失败");
|
|
|
+ chart.setExecuteMessage("AI生成图表失败");
|
|
|
+ updateById(chart);
|
|
|
+ throw new SystemException(500, "AI生成图表失败");
|
|
|
+ }
|
|
|
+ //数据可视化,Echarts的option代码
|
|
|
+ chart.setGeneratedChartData(content.substring(index+7, endIndex).trim());
|
|
|
+ index = endIndex;
|
|
|
+ //分析结论
|
|
|
+ chart.setAnalysisConclusion(content.substring(index+3).trim());
|
|
|
+
|
|
|
+ //更新数据库
|
|
|
+ chart.setState("成功");
|
|
|
+ chart.setExecuteMessage("图表生成成功");
|
|
|
+ updateById(chart);
|
|
|
+ ChartVO chartVO = BeanCopyUtil.copyBean(chart, ChartVO.class);
|
|
|
+ return chartVO;
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ - **异步**
|
|
|
+
|
|
|
+ - ![image-20241215212251850](http://tuchuang.anyi.space/imgs/image-20241215212251850.png)
|
|
|
+
|
|
|
+ - 创建线程池
|
|
|
+
|
|
|
+ - 这个配置类定义了一个自定义的线程池,包括核心线程数、最大线程数、工作队列、线程工厂和拒绝策略。这个线程池可以被应用程序中的其他组件使用,以异步执行任务
|
|
|
+
|
|
|
+ - 线程池参数配置:
|
|
|
+ - `corePoolSize`:核心线程池的大小为1,表示线程池中始终保持的线程数量。
|
|
|
+ - `maximumPoolSize`:最大线程池的大小为2,表示线程池中允许的最大线程数量。
|
|
|
+ - `keepAliveTime`:非核心线程空闲存活时间设置为100秒。
|
|
|
+ - `unit`:时间单位设置为秒。
|
|
|
+ - `workQueue`:工作队列使用`ArrayBlockingQueue`,容量为5,用于存放待执行任务。
|
|
|
+ - `threadFactory`:线程工厂,用于创建新线程。这里没有自定义线程名称或其他属性,直接使用默认的线程创建方式。
|
|
|
+
|
|
|
+ - ```java
|
|
|
+ package space.anyi.BI.config;
|
|
|
+
|
|
|
+ import org.slf4j.Logger;
|
|
|
+ import org.slf4j.LoggerFactory;
|
|
|
+ import org.springframework.context.annotation.Bean;
|
|
|
+ import org.springframework.context.annotation.Configuration;
|
|
|
+
|
|
|
+ import java.util.concurrent.*;
|
|
|
+
|
|
|
+ @Configuration
|
|
|
+ public class ThreadPoolExecutorConfig {
|
|
|
+ private final static Logger log = LoggerFactory.getLogger(ThreadPoolExecutorConfig.class);
|
|
|
+ @Bean
|
|
|
+ public ThreadPoolExecutor getThreadPoolExecutor(){
|
|
|
+ //核心线程数
|
|
|
+ int corePoolSize = 1;
|
|
|
+ //最大线程数
|
|
|
+ int maximumPoolSize = 2;
|
|
|
+ //非核心线程存活空闲时间
|
|
|
+ long keepAliveTime = 100L;
|
|
|
+ //时间单位
|
|
|
+ TimeUnit unit = TimeUnit.SECONDS;
|
|
|
+ //任务队列
|
|
|
+ BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
|
|
|
+ //线程工厂
|
|
|
+ ThreadFactory threadFactory = new ThreadFactory() {
|
|
|
+ @Override
|
|
|
+ public Thread newThread(Runnable r) {
|
|
|
+ Thread thread = new Thread(r);
|
|
|
+ return thread;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ //拒绝策略处理器
|
|
|
+ //RejectedExecutionHandler handler= new ThreadPoolExecutor.CallerRunsPolicy();
|
|
|
+ RejectedExecutionHandler handler= new RejectedExecutionHandler(){
|
|
|
+ @Override
|
|
|
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
|
|
|
+ int activeCount = executor.getActiveCount();
|
|
|
+ int corePoolSize = executor.getCorePoolSize();
|
|
|
+ int maximumPoolSize = executor.getMaximumPoolSize();
|
|
|
+ int queueSize = executor.getQueue().size();
|
|
|
+ log.warn("任务被拒绝执行,当前工作线程数:{},核心线程数:{},最大线程数:{},队列大小:{}",activeCount,corePoolSize,maximumPoolSize,queueSize);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ //创建线程池
|
|
|
+ ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,maximumPoolSize, keepAliveTime,unit,workQueue,threadFactory);
|
|
|
+ return threadPoolExecutor;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ```
|
|
|
+
|
|
|
+ - 使用线程池进行异步优化
|
|
|
+
|
|
|
+ - 实现了异步生成图表的流程,包括读取和验证上传的数据、保存初始图表状态、使用线程池异步执行AI请求、解析响应、数据可视化处理和更新数据库。通过异步处理,可以提高应用的性能,避免在生成图表时阻塞主线程。
|
|
|
+
|
|
|
+ 1. `public long generateChartByAIAsyn(ChartDTO chartDTO, MultipartFile file)`:这是一个公共方法,返回类型是`long`,参数包括一个`ChartDTO`对象和一个`MultipartFile`对象,用于生成图表的异步操作。
|
|
|
+ 2. 初始化图表实体:
|
|
|
+ - 使用`BeanCopyUtil.copyBean`方法将`ChartDTO`对象的属性复制到一个新的`Chart`实体对象中。
|
|
|
+ - 生成一个唯一的图表ID,并设置到`Chart`实体对象中。
|
|
|
+ - 设置用户ID和图表状态为“等待中”。
|
|
|
+ 3. 读取数据:
|
|
|
+ - 尝试将上传的Excel文件转换为CSV格式的字符串数据,并记录日志。
|
|
|
+ - 如果转换过程中发生`IOException`,则打印堆栈跟踪。
|
|
|
+ 4. 数据量检查:
|
|
|
+ - 如果CSV数据长度超过3000字符,则更新图表状态为“失败”,保存到数据库,并抛出异常。
|
|
|
+ 5. 保存图表实体:
|
|
|
+ - 将CSV数据设置为图表实体的`chartData`属性,并保存到数据库。
|
|
|
+ 6. 异步处理:
|
|
|
+ - 使用线程池`threadPoolExecutor`来异步执行图表生成逻辑。
|
|
|
+ 7. 异步执行的逻辑:
|
|
|
+ - 更新图表状态为“生成中”并保存。
|
|
|
+ - 构建请求消息,包含原始数据、分析目标和图表类型。
|
|
|
+ - 发送请求到AI服务,并接收响应数据。
|
|
|
+ - 解析AI返回的数据,提取图表生成的内容和分析结论。
|
|
|
+ - 如果AI返回的数据格式不正确(没有找到预期的```标记),则更新图表状态为“失败”并抛出异常。
|
|
|
+ - 提取Echarts的option代码和分析结论,并更新图表实体的相关属性。
|
|
|
+ - 更新图表状态为“成功”并保存。
|
|
|
+ 8. 返回结果:
|
|
|
+ - 返回生成的图表ID
|
|
|
+
|
|
|
+ - ```java
|
|
|
+ @Override
|
|
|
+ public long generateChartByAIAsyn(ChartDTO chartDTO, MultipartFile file) {
|
|
|
+
|
|
|
+ Chart chart = BeanCopyUtil.copyBean(chartDTO, Chart.class);
|
|
|
+ long chartId = IdUtil.getSnowflake(1, 1).nextId();
|
|
|
+ chart.setId(chartId);
|
|
|
+ chart.setUserId(SecurityUtils.getUserId());
|
|
|
+ chart.setState("等待中");
|
|
|
+ //读数据
|
|
|
+ String csvData = "";
|
|
|
+ try {
|
|
|
+ csvData = ExcelUtils.excel2csv(file.getInputStream());
|
|
|
+ log.info("上传的数据为:\n{}", csvData);
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ if (csvData.length()>3000){
|
|
|
+ chart.setChartData("");
|
|
|
+ chart.setState("失败");
|
|
|
+ chart.setExecuteMessage("数据量过大,请上传小于3000行的数据");
|
|
|
+ save(chart);
|
|
|
+ throw new SystemException(500, "数据量过大,请上传小于3000行的数据");
|
|
|
+ }
|
|
|
+ chart.setChartData(csvData);
|
|
|
+ save(chart);
|
|
|
+ //使用线程池优化生成图表的逻辑
|
|
|
+ String finalCsvData = csvData;
|
|
|
+ threadPoolExecutor.execute(()->{
|
|
|
+ chart.setState("生成中");
|
|
|
+ updateById(chart);
|
|
|
+
|
|
|
+ StringBuilder message = new StringBuilder("原始数据:\n");
|
|
|
+ message.append(finalCsvData);
|
|
|
+ message.append("分析目标:\n");
|
|
|
+ message.append(chartDTO.getAnalysisTarget());
|
|
|
+ message.append("\n.使用").append(chartDTO.getChartType()).append("进行可视化分析.\n");
|
|
|
+ //配置prompt向AI发送请求
|
|
|
+ HttpRequestData requestData = AiUtil.createDefaultRequestData(message.toString());
|
|
|
+ HttpResponseData responseData = AiUtil.doChat(requestData);
|
|
|
+ //解析AI返回的数据
|
|
|
+ String content = responseData.getChoices().get(0).getMessage().getContent();
|
|
|
+ log.info("AI返回的数据为:{}", content);
|
|
|
+ int index = content.indexOf("```");
|
|
|
+ int endIndex = content.lastIndexOf("```");
|
|
|
+ if (index == -1 || endIndex == -1){
|
|
|
+ chart.setState("失败");
|
|
|
+ chart.setExecuteMessage("AI生成图表失败");
|
|
|
+ updateById(chart);
|
|
|
+ throw new SystemException(500, "AI生成图表失败");
|
|
|
+ }
|
|
|
+ //数据可视化,Echarts的option代码
|
|
|
+ chart.setGeneratedChartData(content.substring(index+7, endIndex).trim());
|
|
|
+ index = endIndex;
|
|
|
+ //分析结论
|
|
|
+ chart.setAnalysisConclusion(content.substring(index+3).trim());
|
|
|
+ //保存到数据库
|
|
|
+ chart.setState("成功");
|
|
|
+ chart.setExecuteMessage("AI生成图表成功");
|
|
|
+ updateById(chart);
|
|
|
+ });
|
|
|
+
|
|
|
+ return chartId;
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+- **限流**:使用Redisson实现令牌桶算法,通过RRateLimiterHandler进行接口限流。
|
|
|
+
|
|
|
+ - 这个服务类提供了一个简单的限流功能,通过Redisson客户端实现。它允许开发者为特定的请求设置一个限流器,并且根据设定的速率来控制请求的通过。
|
|
|
+
|
|
|
+ - `tryAcquire`方法是非阻塞的,它会立即返回一个布尔值,指示是否成功获取到令牌
|
|
|
+
|
|
|
+ ```java
|
|
|
+ package space.anyi.BI.handler.redisson;
|
|
|
+
|
|
|
+ import org.redisson.api.RRateLimiter;
|
|
|
+ import org.redisson.api.RateIntervalUnit;
|
|
|
+ import org.redisson.api.RateType;
|
|
|
+ import org.redisson.api.RedissonClient;
|
|
|
+ import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+ import javax.annotation.Resource;
|
|
|
+
|
|
|
+ @Service
|
|
|
+ public class RRateLimiterHandler {
|
|
|
+ @Resource
|
|
|
+ private RedissonClient redissonClient;
|
|
|
+ public boolean accessAble(String key){
|
|
|
+ //获取限流器
|
|
|
+ RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
|
|
|
+ //设置限流器速率
|
|
|
+ //rateLimiter.setRate(RateType.OVERALL,5,1, RateIntervalUnit.SECONDS);
|
|
|
+ rateLimiter.trySetRate(RateType.OVERALL,5,1, RateIntervalUnit.SECONDS);
|
|
|
+ //获取令牌,每次获取一个令牌,如果获取不到则返回false
|
|
|
+ //return rateLimiter.tryAcquire(1);
|
|
|
+ //阻塞获取令牌,每次获取一个令牌
|
|
|
+ //rateLimiter.acquire();
|
|
|
+ return rateLimiter.tryAcquire(1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+- 调用AI进行数据分析
|
|
|
+
|
|
|
+ - ```java
|
|
|
+ package space.anyi.BI.util;
|
|
|
+
|
|
|
+ import cn.hutool.http.HttpResponse;
|
|
|
+ import cn.hutool.http.HttpUtil;
|
|
|
+ import com.fasterxml.jackson.core.JsonProcessingException;
|
|
|
+ import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+ import space.anyi.BI.entity.ResponseResult;
|
|
|
+ import space.anyi.BI.entity.xinghuo.HttpRequestData;
|
|
|
+ import space.anyi.BI.entity.xinghuo.HttpRequestMessage;
|
|
|
+ import space.anyi.BI.entity.xinghuo.HttpResponseData;
|
|
|
+ import space.anyi.BI.exception.SystemException;
|
|
|
+
|
|
|
+ import java.util.ArrayList;
|
|
|
+ import java.util.List;
|
|
|
+
|
|
|
+ public class AiUtil {
|
|
|
+ private static final String url = "https://spark-api-open.xf-yun.com/v1/chat/completions";
|
|
|
+ public static ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用星火AI接口
|
|
|
+ * @param requestData
|
|
|
+ * @return {@code HttpResponseData }
|
|
|
+ * @description:
|
|
|
+ * @author: 杨逸
|
|
|
+ * @data:2024/12/04 20:50:06
|
|
|
+ * @since 1.0.0
|
|
|
+ */
|
|
|
+ public static HttpResponseData doChat(HttpRequestData requestData){
|
|
|
+ String json = null;
|
|
|
+ try {
|
|
|
+ json = objectMapper.writeValueAsString(requestData);
|
|
|
+ } catch (JsonProcessingException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ HttpResponse httpResponse = HttpUtil.createPost(url)
|
|
|
+ .header("Content-Type", "application/json")
|
|
|
+ .header("Authorization", "Bearer token")
|
|
|
+ .body(json)
|
|
|
+ .execute();
|
|
|
+ String body = httpResponse.body();
|
|
|
+ int index = body.indexOf('0');
|
|
|
+ HttpResponseData httpResponseData = null;
|
|
|
+ if (index == 8){
|
|
|
+ //调用成功
|
|
|
+ try {
|
|
|
+ httpResponseData = objectMapper.readValue(body, HttpResponseData.class);
|
|
|
+ } catch (JsonProcessingException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }else{
|
|
|
+ System.err.println(body);
|
|
|
+ throw new SystemException(ResponseResult.AppHttpCodeEnum.SYSTEM_ERROR);
|
|
|
+ }
|
|
|
+ return httpResponseData;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建一个简单的请求体
|
|
|
+ * @param message
|
|
|
+ * @return {@code HttpRequestData }
|
|
|
+ * @description:
|
|
|
+ * @author: 杨逸
|
|
|
+ * @data:2024/12/04 20:54:46
|
|
|
+ * @since 1.0.0
|
|
|
+ */
|
|
|
+ public static HttpRequestData createDefaultRequestData(String message){
|
|
|
+ HttpRequestData httpRequestData = new HttpRequestData();
|
|
|
+ List<HttpRequestMessage> messages = new ArrayList<>();
|
|
|
+ messages.add(HttpRequestMessage.getPromptMessage());
|
|
|
+ messages.add(new HttpRequestMessage(HttpRequestMessage.USER_ROLE, message));
|
|
|
+ httpRequestData.setMessages(messages);
|
|
|
+ return httpRequestData;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ```
|
|
|
+
|
|
|
+#### 前端
|
|
|
+
|
|
|
+![image-20241215213453618](http://tuchuang.anyi.space/imgs/image-20241215213453618.png)
|
|
|
+
|
|
|
+- 提供了一个完整的用户界面,用于上传数据、配置图表生成参数、提交表单、显示图表和分析结论
|
|
|
+
|
|
|
+ ### `<template>` 部分
|
|
|
+
|
|
|
+ 1. 使用`el-row`和`el-col`创建布局,分为三个主要部分:表单区域、分隔符和图表显示区域。
|
|
|
+ 2. 在表单区域中,使用`el-form`创建一个表单,包含图表名称、分析目标、图表类型、异步分析选项和文件上传等字段。
|
|
|
+ 3. 图表类型使用`el-select`下拉选择框,异步分析使用`el-switch`开关。
|
|
|
+ 4. 文件上传使用`el-upload`组件,限制上传文件大小不超过2MB,并且只能是`.xls`或`.xlsx`格式。
|
|
|
+ 5. 提交和重置按钮用于提交表单或重置表单数据。
|
|
|
+ 6. 图表显示区域中,使用`div`元素作为图表的容器,并在底部提供了一个按钮用于获取分析结果。
|
|
|
+ 7. 分析结论部分,显示分析的结论文本。
|
|
|
+
|
|
|
+ ### `<script>` 部分
|
|
|
+
|
|
|
+ 1. 导入了生成图表和获取图表结果的API方法,以及`ElMessage`用于显示消息提示。
|
|
|
+ 2. 定义了组件的数据结构,包括表单数据、图表配置、分析结论等。
|
|
|
+ 3. 定义了表单验证规则。
|
|
|
+ 4. 定义了文件上传的action地址,这里使用的是一个占位符URL,实际开发中需要替换为真实的上传接口。
|
|
|
+ 5. 提供了`submitForm`方法用于提交表单,根据是否异步分析调用不同的API,并处理响应结果。
|
|
|
+ 6. `changeFile`方法用于更新表单中的文件数据。
|
|
|
+ 7. `resetForm`方法用于重置表单。
|
|
|
+ 8. `fileBeforeUpload`方法用于在文件上传前进行校验,确保文件大小和类型符合要求。
|
|
|
+ 9. `chartRefresh`方法用于初始化或刷新图表。
|
|
|
+ 10. `getChartResult`方法用于获取异步分析的结果,并更新图表和分析结论。
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card>
|
|
|
+ <template #header>11</template>
|
|
|
+ <template #default>
|
|
|
+ <el-form ref="chartForm" :model="formData" :rules="rules" size="medium" label-width="100px"
|
|
|
+ label-position="top">
|
|
|
+ <el-form-item label="图表名称" prop="name">
|
|
|
+ <el-input v-model="formData.name" placeholder="请输入图表名称" clearable :style="{width: '100%'}"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="分析目标" prop="analysisTarget">
|
|
|
+ <el-input v-model="formData.analysisTarget" type="textarea" placeholder="请输入分析目标;比如:分析网站的增长趋势"
|
|
|
+ :autosize="{minRows: 4, maxRows: 4}" :style="{width: '100%'}"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="图表类型" prop="chartType">
|
|
|
+ <el-select v-model="formData.chartType" placeholder="请选择图表类型" clearable :style="{width: '80%'}">
|
|
|
+ <el-option v-for="(item, index) in chartTypeOptions" :key="index" :label="item.label"
|
|
|
+ :value="item.value" :disabled="item.disabled"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="异步分析" prop="isAsynchronism">
|
|
|
+ <el-switch v-model="formData.isAsynchronism" @change="console.log(formData.isAsynchronism)"/>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-form-item label="上传原始数据" prop="file" required>
|
|
|
+ <el-upload ref="file" :file-list="filefileList" :action="fileAction" :auto-upload="false" limit="1"
|
|
|
+ :before-upload="fileBeforeUpload" @change="changeFile" accept=".xls,.xlsx">
|
|
|
+ <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
|
|
|
+ <div slot="tip" class="el-upload__tip">只能上传不超过 2MB 的.xls,.xlsx文件</div>
|
|
|
+ </el-upload>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item size="large">
|
|
|
+ <el-button type="primary" @click="submitForm">提交</el-button>
|
|
|
+ <el-button @click="resetForm">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </template>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1">
|
|
|
+ <!--<el-divider direction="vertical" style="height: 100vh"/>-->
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="15">
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="24">
|
|
|
+ <el-card>
|
|
|
+ <template #header>
|
|
|
+ 数据图表
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ <div ref="chart" style="height:50vh"/>
|
|
|
+ </template>
|
|
|
+ <template #footer v-if="chartId">
|
|
|
+ <el-button type="primary" @click.prevent="getChartResult">获取分析结果</el-button>
|
|
|
+ </template>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="24">
|
|
|
+ <el-card>
|
|
|
+ <template #header>分析结论</template>
|
|
|
+ <template #default>
|
|
|
+ {{analysisConclusion}}
|
|
|
+ </template>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import {generateChartByAI, getChart} from "../common/api/chart/index.js";
|
|
|
+import {ElMessage} from "element-plus";
|
|
|
+import * as echarts from 'echarts';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'generateChart',
|
|
|
+ components: {},
|
|
|
+ props: [],
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ formData: {
|
|
|
+ name: undefined,
|
|
|
+ analysisTarget: undefined,
|
|
|
+ chartType: undefined,
|
|
|
+ file: null,
|
|
|
+ isAsynchronism:false
|
|
|
+ },
|
|
|
+ option:{
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: ['2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19'],
|
|
|
+ boundaryGap: true,
|
|
|
+ axisLabel:{
|
|
|
+ interval:0,
|
|
|
+ rotate : 50
|
|
|
+ },
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value'
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ name:'增长人数',
|
|
|
+ type:'line',
|
|
|
+ data:[34.0, 34.0, 3.0, 3.0, 43.0, 34.0, 43.0, 34.0, 34.0, 12.0, 12.0, 12.0, 12.0, 32.0, 12.0, 32.0, 32.0, 32.0, 32.0],
|
|
|
+ markPoint: {
|
|
|
+ data: [
|
|
|
+ {type: 'max', name: '最大值'},
|
|
|
+ {type: 'min', name: '最小值'}
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ markLine: {data: [
|
|
|
+ {type: 'average', name: '平均值'}
|
|
|
+ ]},
|
|
|
+ smooth: true
|
|
|
+ }]
|
|
|
+ },
|
|
|
+ analysisConclusion:'',
|
|
|
+ chart:{},
|
|
|
+ chartId:undefined,
|
|
|
+ rules: {
|
|
|
+ name: [{
|
|
|
+ required: true,
|
|
|
+ message: '请输入图表名称',
|
|
|
+ trigger: 'blur'
|
|
|
+ }],
|
|
|
+ analysisTarget: [{
|
|
|
+ required: true,
|
|
|
+ message: '请输入分析目标;比如:分析网站的增长趋势',
|
|
|
+ trigger: 'blur'
|
|
|
+ }],
|
|
|
+ chartType: [{
|
|
|
+ required: true,
|
|
|
+ message: '请选择图表类型',
|
|
|
+ trigger: 'change'
|
|
|
+ }],
|
|
|
+ },
|
|
|
+ fileAction: 'https://jsonplaceholder.typicode.com/posts/',
|
|
|
+ filefileList: [],
|
|
|
+ chartTypeOptions: [{
|
|
|
+ "label": "折线图",
|
|
|
+ "value": "折线图"
|
|
|
+ }, {
|
|
|
+ "label": "柱状图",
|
|
|
+ "value": "柱状图"
|
|
|
+ }, {
|
|
|
+ "label": "饼图",
|
|
|
+ "value": "饼图"
|
|
|
+ }, {
|
|
|
+ "label": "热力图",
|
|
|
+ "value": "热力图"
|
|
|
+ },{
|
|
|
+ "label": "散点图",
|
|
|
+ "value": "散点图"
|
|
|
+ },{
|
|
|
+ "label": "雷达图",
|
|
|
+ "value": "雷达图"
|
|
|
+ }],
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {},
|
|
|
+ watch: {},
|
|
|
+ created() {
|
|
|
+
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.chartRefresh();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ submitForm() {
|
|
|
+ console.log(this.formData)
|
|
|
+ this.$refs['chartForm'].validate(valid => {
|
|
|
+ if (!valid) return
|
|
|
+ if (this.formData.isAsynchronism) {
|
|
|
+ generateChartByAI(this.formData).then(res =>{
|
|
|
+ if (res.code == 200){
|
|
|
+ console.log(res)
|
|
|
+ this.chartId = res.data;
|
|
|
+ ElMessage({
|
|
|
+ message: res.msg,
|
|
|
+ type: 'success',
|
|
|
+ });
|
|
|
+ }else{
|
|
|
+ ElMessage({
|
|
|
+ message: res.msg,
|
|
|
+ type: 'error',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }).catch(err=>{
|
|
|
+ ElMessage({
|
|
|
+ message: err,
|
|
|
+ type: 'error',
|
|
|
+ });
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ this.chartId = undefined;
|
|
|
+ generateChartByAI(this.formData).then(res => {
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.analysisConclusion = res.data.analysisConclusion;
|
|
|
+ //取出配置对象的代码
|
|
|
+ let generatedChartData = res.data.generatedChartData;
|
|
|
+ //将配置对象的代码转换为JSON对象,不是不是标准的JSON对象,需要使用eval函数进行转换
|
|
|
+ // let optionJson = JSON.parse(option);
|
|
|
+ let optionJson = eval("(" + generatedChartData + ")")
|
|
|
+ //返回格式不稳定,有可能会多封装一层option,需要判断一下
|
|
|
+ if (optionJson.option === undefined || optionJson.option === null) {
|
|
|
+ this.option = optionJson;
|
|
|
+ } else {
|
|
|
+ this.option = optionJson.option;
|
|
|
+ }
|
|
|
+ this.chartRefresh();
|
|
|
+ ElMessage({
|
|
|
+ message: res.msg,
|
|
|
+ type: 'success',
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ ElMessage({
|
|
|
+ message: res.msg,
|
|
|
+ type: 'error',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }).catch(err => {
|
|
|
+ ElMessage({
|
|
|
+ message: err,
|
|
|
+ type: 'error',
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ changeFile(file){
|
|
|
+ this.formData.file = file.raw;
|
|
|
+ },
|
|
|
+ resetForm() {
|
|
|
+ this.$refs['chartForm'].resetFields()
|
|
|
+ },
|
|
|
+ fileBeforeUpload(file) {
|
|
|
+ let isRightSize = file.size / 1024 / 1024 < 2
|
|
|
+ if (!isRightSize) {
|
|
|
+ this.$message.error('文件大小超过 2MB')
|
|
|
+ }
|
|
|
+ let isAccept = new RegExp('.xls,.xlsx').test(file.type)
|
|
|
+ if (!isAccept) {
|
|
|
+ this.$message.error('应该选择.xls,.xlsx类型的文件')
|
|
|
+ }
|
|
|
+ return isRightSize && isAccept
|
|
|
+ },
|
|
|
+ chartRefresh(){
|
|
|
+ setTimeout(()=>{
|
|
|
+ this.chart = echarts.init(this.$refs.chart);
|
|
|
+ this.chart.clear();
|
|
|
+ setTimeout(()=>{
|
|
|
+ this.chart.setOption(this.option);
|
|
|
+ },300)
|
|
|
+ },200)
|
|
|
+ },
|
|
|
+ getChartResult(){
|
|
|
+ getChart(this.chartId).then(res =>{
|
|
|
+ if (res.code == 200 && res.data.state == '成功'){
|
|
|
+ this.analysisConclusion = res.data.analysisConclusion;
|
|
|
+ //取出配置对象的代码
|
|
|
+ let generatedChartData = res.data.generatedChartData;
|
|
|
+ //将配置对象的代码转换为JSON对象,不是不是标准的JSON对象,需要使用eval函数进行转换
|
|
|
+ let optionJson = eval("(" + generatedChartData + ")")
|
|
|
+ //返回格式不稳定,有可能会多封装一层option,需要判断一下
|
|
|
+ if (optionJson.option === undefined || optionJson.option === null) {
|
|
|
+ this.option = optionJson;
|
|
|
+ } else {
|
|
|
+ this.option = optionJson.option;
|
|
|
+ }
|
|
|
+ this.chartRefresh();
|
|
|
+ ElMessage({
|
|
|
+ message: '生成图表成功',
|
|
|
+ type: 'success',
|
|
|
+ });
|
|
|
+ this.chartId = undefined;
|
|
|
+ }else if (res.code == 200){
|
|
|
+ ElMessage({
|
|
|
+ message: res.data.state,
|
|
|
+ type: 'info',
|
|
|
+ });
|
|
|
+ }else {
|
|
|
+ ElMessage({
|
|
|
+ message: res.msg,
|
|
|
+ type: 'error',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }).catch(err =>{
|
|
|
+ ElMessage({
|
|
|
+ message: err,
|
|
|
+ type: 'error',
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.el-upload__tip {
|
|
|
+ line-height: 1.2;
|
|
|
+}
|
|
|
+
|
|
|
+</style>
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+### 4.3 图表管理
|
|
|
+
|
|
|
+![image-20241215215126088](http://tuchuang.anyi.space/imgs/image-20241215215126088.png)
|
|
|
+
|
|
|
+- **图表展示**:前端使用ECharts展示图表,用户可以查看和操作图表。
|
|
|
+ - ![image-20241215215311518](http://tuchuang.anyi.space/imgs/image-20241215215311518.png)
|
|
|
+- **图表删除**:删除图表
|
|
|
+
|
|
|
+### 4.4 安全认证
|
|
|
+
|
|
|
+- **JWTAuthenticationTokenFilter**:自定义过滤器,用于验证请求中的JWT。
|
|
|
+
|
|
|
+- **异常处理**:GlobalExceptionHandler处理认证和鉴权过程中的异常。
|
|
|
+
|
|
|
+ - 全局异常处理器定义了两个方法,分别用于处理自定义的`SystemException`异常和所有其他类型的`Exception`异常。通过这种方式,可以统一异常处理逻辑,确保所有异常都能被恰当地记录和响应,提高应用程序的健壮性和用户体验。
|
|
|
+
|
|
|
+ 1. `@RestControllerAdvice`:这个注解表示这个类是一个全局的异常处理控制器,它会对整个应用程序范围内的异常进行捕捉和处理。
|
|
|
+ 2. `GlobalExceptionHandler` 类:定义了一个全局异常处理器。
|
|
|
+ 3. `private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);`:创建一个日志对象,用于记录日志信息。
|
|
|
+ 4. `@ExceptionHandler(SystemException.class)`:这个注解指定了方法`systemException`用于处理`SystemException`类型的异常。
|
|
|
+ 5. `systemException(SystemException e)` 方法:处理`SystemException`异常。
|
|
|
+ - `log.error("发生错误:{}",e);`:记录错误日志,包括异常堆栈跟踪。
|
|
|
+ - `return ResponseResult.errorResult(e.getCode(),e.getMsg());`:返回一个`ResponseResult`对象,包含错误码和错误消息。
|
|
|
+ 6. `@ExceptionHandler(Exception.class)`:这个注解指定了方法`systemException`用于处理所有未被捕获的`Exception`类型的异常。
|
|
|
+ 7. `systemException(Exception e)` 方法:处理通用`Exception`异常。
|
|
|
+ - `log.error("发生错误:{}",e);`:记录错误日志,包括异常堆栈跟踪。
|
|
|
+ - `return ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());`:返回一个`ResponseResult`对象,包含系统错误码和异常消息。
|
|
|
+
|
|
|
+ - ```java
|
|
|
+ package space.anyi.BI.handler.exception;
|
|
|
+
|
|
|
+ import org.slf4j.Logger;
|
|
|
+ import org.slf4j.LoggerFactory;
|
|
|
+ import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
|
+ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
|
+ import space.anyi.BI.entity.ResponseResult;
|
|
|
+ import space.anyi.BI.exception.SystemException;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @ProjectName: BI
|
|
|
+ * @FileName: GlobalExceptionHandler
|
|
|
+ * @Author: 杨逸
|
|
|
+ * @Data:2024/11/28 20:25
|
|
|
+ * @Description:
|
|
|
+ */
|
|
|
+ @RestControllerAdvice
|
|
|
+ public class GlobalExceptionHandler{
|
|
|
+ private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
|
|
+ @ExceptionHandler(SystemException.class)
|
|
|
+ public ResponseResult systemException(SystemException e){
|
|
|
+ log.error("发生错误:{}",e);
|
|
|
+ return ResponseResult.errorResult(e.getCode(),e.getMsg());
|
|
|
+ }
|
|
|
+ @ExceptionHandler(Exception.class)
|
|
|
+ public ResponseResult systemException(Exception e){
|
|
|
+ log.error("发生错误:{}",e);
|
|
|
+ return ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+- **认证异常处理**:
|
|
|
+
|
|
|
+ - 这个类定义了认证失败后的处理逻辑,根据不同的认证异常返回不同的错误信息,并以JSON格式响应给客户端
|
|
|
+
|
|
|
+ 1. `AuthenticationEntryPointImpl` 类:实现了`AuthenticationEntryPoint`接口,用于自定义处理认证失败后的行为。
|
|
|
+ 2. `commence` 方法:当Spring Security认证失败时,这个方法会被调用。它接收`HttpServletRequest`和`HttpServletResponse`对象,以及一个`AuthenticationException`异常对象。
|
|
|
+ 3. 异常处理:
|
|
|
+ - `InsufficientAuthenticationException`:当认证信息不足时抛出的异常,例如,用户未提供认证信息。针对这种异常,返回一个特定的错误码`NO_OPERATOR_AUTH`。
|
|
|
+ - `InternalAuthenticationServiceException`:当认证过程中发生内部错误时抛出的异常,例如,密码错误。针对这种异常,返回一个错误码`LOGIN_ERROR`。
|
|
|
+ - 其他`AuthenticationException`:对于其他类型的认证异常,返回系统错误码`SYSTEM_ERROR`,并附带异常消息。
|
|
|
+ 4. 响应处理:
|
|
|
+ - 使用`WebUtils.renderString`方法将错误信息以JSON格式返回给客户端。这里使用`JSON.toJSONString(responseResult)`将`ResponseResult`对象转换为JSON字符串。
|
|
|
+ 5. 异常堆栈跟踪打印:
|
|
|
+ - `e.printStackTrace();`:打印异常堆栈跟踪.
|
|
|
+
|
|
|
+ - ```java
|
|
|
+ package space.anyi.BI.handler.security;
|
|
|
+
|
|
|
+ import com.alibaba.fastjson.JSON;
|
|
|
+ import org.springframework.security.authentication.InsufficientAuthenticationException;
|
|
|
+ import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
|
|
+ import org.springframework.security.core.AuthenticationException;
|
|
|
+ import org.springframework.security.web.AuthenticationEntryPoint;
|
|
|
+ import org.springframework.stereotype.Component;
|
|
|
+ import space.anyi.BI.entity.ResponseResult;
|
|
|
+ import space.anyi.BI.util.WebUtils;
|
|
|
+
|
|
|
+ import javax.servlet.ServletException;
|
|
|
+ import javax.servlet.http.HttpServletRequest;
|
|
|
+ import javax.servlet.http.HttpServletResponse;
|
|
|
+ import java.io.IOException;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @ProjectName: BI
|
|
|
+ * @FileName: AuthenticationEntryPointImpl
|
|
|
+ * @Author: 杨逸
|
|
|
+ * @Data:2024/11/28 20:18
|
|
|
+ * @Description:
|
|
|
+ */
|
|
|
+ @Component
|
|
|
+ public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
|
|
|
+ @Override
|
|
|
+ public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
|
|
|
+ e.printStackTrace();
|
|
|
+ ResponseResult responseResult = null;
|
|
|
+ //针对不同异常,返回对应的信息
|
|
|
+ if (e instanceof InsufficientAuthenticationException) {
|
|
|
+ responseResult = ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.NO_OPERATOR_AUTH);
|
|
|
+ } else if (e instanceof InternalAuthenticationServiceException) {
|
|
|
+ responseResult = ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.LOGIN_ERROR);
|
|
|
+ } else {
|
|
|
+ responseResult = ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());
|
|
|
+ }
|
|
|
+ WebUtils.renderString(httpServletResponse, JSON.toJSONString(responseResult));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+- **鉴权异常处理**:
|
|
|
+
|
|
|
+ - 这个类定义了用户访问被拒绝后的处理逻辑,返回一个错误信息给前端,并以JSON格式响应
|
|
|
+
|
|
|
+ 1. `AccessDeniedHandlerImpl` 类:实现了`AccessDeniedHandler`接口,用于自定义处理用户访问被拒绝后的行为。
|
|
|
+ 2. `handle` 方法:当用户的访问被拒绝时,这个方法会被调用。它接收`HttpServletRequest`和`HttpServletResponse`对象,以及一个`AccessDeniedException`异常对象。
|
|
|
+ 3. 异常处理:
|
|
|
+ - `e.printStackTrace();`:打印异常堆栈跟踪,这通常用于调试目的。
|
|
|
+ 4. 响应处理:
|
|
|
+ - 创建一个`ResponseResult`对象,使用`ResponseResult.errorResult`方法,并传入错误码`NO_OPERATOR_AUTH`,表示用户没有操作权限。
|
|
|
+ - 使用`WebUtils.renderString`方法将`ResponseResult`对象转换为JSON字符串,并写入到`HttpServletResponse`中,作为HTTP响应返回给客户端。
|
|
|
+ 5. 异常信息:
|
|
|
+ - `AccessDeniedException`:当用户尝试访问他们没有权限的资源时,Spring Security会抛出这个异常。
|
|
|
+
|
|
|
+ - ```java
|
|
|
+ package space.anyi.BI.handler.security;
|
|
|
+
|
|
|
+ import com.alibaba.fastjson.JSON;
|
|
|
+ import org.springframework.security.access.AccessDeniedException;
|
|
|
+ import org.springframework.security.web.access.AccessDeniedHandler;
|
|
|
+ import org.springframework.stereotype.Component;
|
|
|
+ import space.anyi.BI.entity.ResponseResult;
|
|
|
+ import space.anyi.BI.util.WebUtils;
|
|
|
+
|
|
|
+ import javax.servlet.ServletException;
|
|
|
+ import javax.servlet.http.HttpServletRequest;
|
|
|
+ import javax.servlet.http.HttpServletResponse;
|
|
|
+ import java.io.IOException;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @ProjectName: BI
|
|
|
+ * @FileName: AccessDeniedHandlerImpl
|
|
|
+ * @Author: 杨逸
|
|
|
+ * @Data:2024/11/28 20:17
|
|
|
+ * @Description:
|
|
|
+ */
|
|
|
+ @Component
|
|
|
+ public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
|
|
|
+ @Override
|
|
|
+ public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
|
|
|
+ e.printStackTrace();
|
|
|
+ ResponseResult responseResult = ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.NO_OPERATOR_AUTH);
|
|
|
+ WebUtils.renderString(httpServletResponse, JSON.toJSONString(responseResult));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ```
|
|
|
+
|
|
|
+
|
|
|
+
|