# 智能数据分析系统设计开发与实现文档 ## 一.项目简介 [线上](http://dataanalysis.anyi.space/index) ### 1.1 项目背景 随着大数据时代的到来,数据分析变得越来越重要。智能数据分析平台旨在为用户提供一个简单易用、功能强大的数据分析工具,帮助用户从海量数据中提取有价值的信息,并通过图表直观展示,区别于传统 BI,用户只需要导入原始数据集、并输入分析诉求,就能自动生成可视化图表及分析结论,实现数据分析的降本增效。 ### 1.2 项目特点 智能数据分析平台是一个基于Spring Boot后端和Vue.js前端的前后端分离应用,通过JWT实现身份验证,采用RBAC进行权限管理,并提供图表生成和管理功能。项目选题**新颖**,不同于泛滥的管理系统、博客、商城、本项目是结合**AIGC技术+企业BI业务场景**的综合实战,紧跟时代潮流。 ![img](https://pic.yupi.icu/1/(null)-20231026151931197.(null)) ## 二.需求分析 ### 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架构,前端使用Vue3.js构建用户界面,后端使用Spring Boot构建RESTful API **前端** - Vue3.js - Element Plus组件库 - ECharts可视化库 **后端** - Java Spring Boot - Java Spring Security - MySQL 数据库 - MyBatis-Plus - Redis + Redisson 限流 - JWT令牌校验 - Easy Excel 表格数据处理 - Swagger + Knife4j 接口文档生成 - Hutool、Apache Common Utils 等工具库 ### 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 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`对象,包含了查询结果和分页信息。 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 userVOList = new ArrayList<>(); LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper() .like(StringUtils.hasText(userName), User::getUserName, userName) .eq(StringUtils.hasText(userAccount), User::getUserAccount, userAccount) .eq(StringUtils.hasText(userRole), User::getUserRole, userRole); Page page = page(new Page(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 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 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 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 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 userVOList = new ArrayList<>(); LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper() .like(StringUtils.hasText(userName), User::getUserName, userName) .eq(StringUtils.hasText(userAccount), User::getUserAccount, userAccount) .eq(StringUtils.hasText(userRole), User::getUserRole, userRole); Page page = page(new Page(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 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) - **功能**:用户的登陆和注册 - 元素 - 登陆表单 ### `