yang yi ff37761cc3 document:文档修改 | 4 tygodni temu | |
---|---|---|
BI_front | 4 tygodni temu | |
document | 4 tygodni temu | |
serve | 4 tygodni temu | |
.gitignore | 4 tygodni temu | |
readme.md | 4 tygodni temu |
随着大数据时代的到来,数据分析变得越来越重要。智能数据分析平台旨在为用户提供一个简单易用、功能强大的数据分析工具,帮助用户从海量数据中提取有价值的信息,并通过图表直观展示,区别于传统 BI,用户只需要导入原始数据集、并输入分析诉求,就能自动生成可视化图表及分析结论,实现数据分析的降本增效。
智能数据分析平台是一个基于Spring Boot后端和Vue.js前端的前后端分离应用,通过JWT实现身份验证,采用RBAC进行权限管理,并提供图表生成和管理功能。项目选题新颖,不同于泛滥的管理系统、博客、商城、本项目是结合AIGC技术+企业BI业务场景的综合实战,紧跟时代潮流。
系统采用B/S架构,前端使用Vue3.js构建用户界面,后端使用Spring Boot构建RESTful API
前端
后端
数据库使用MySQL,设计以下表结构:
角色:用户角色。
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='用户表';
图表数据:图表的JSON配置数据。
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='图表表';
使用Swagger文档化API接口。
POST /user/login
:用户登录。POST /user/register
:用户注册。GET /user/getUSerInfo
:获取用户信息GET /user/page
:用户信息分页POST /user
:新增用户PUT /user
:更新用户信息GET /user/getUserBiId/{id}
:获取用户信息DELETE /user
/{ids}:删除用户接口地址:/user/login
请求方式:POST
请求数据类型:application/json
响应数据类型:*/*
接口描述:用户登录
请求示例:
{
"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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/user/register
请求方式:POST
请求数据类型:application/json
响应数据类型:*/*
接口描述:用户注册
请求示例:
{
"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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/user
请求方式:POST
请求数据类型:application/json
响应数据类型:*/*
请求示例:
{
"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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/user
请求方式:PUT
请求数据类型:application/json
响应数据类型:*/*
请求示例:
{
"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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
POST /chart/generateChartByAI
:生成图表。GET /chart/getChartById/{id}
:获取图表详情。GET /chart/list
:获取图表列表POST /chart/add
:新增图表PUT /chart/update
:更新图表DELETE /chart/{ids}
:删除图表接口地址:/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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/chart/add
请求方式:POST
请求数据类型:application/json
响应数据类型:*/*
请求示例:
{
"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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/chart/update
请求方式:PUT
请求数据类型:application/json
响应数据类型:*/*
请求示例:
{
"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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
接口地址:/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 |
响应示例:
{
"code": 0,
"data": {},
"msg": ""
}
使用JWT进行用户认证,Spring Security实现权限控制。
登录与注册:用户可以通过UserController进行登录和注册,系统使用JWT进行身份验证。
权限控制:基于角色的访问控制,使用Spring Security和自定义的权限注解实现。
POST /user/login
UserController
当客户端向/login
路径发送POST请求并包含用户信息(如用户名和密码)时,执行登录逻辑,并返回一个包含登录结果的ResponseResult
对象。
@PostMapping("/login")
public ResponseResult login(@RequestBody User user){
return userService.login(user);
}
UserServce
实现了用户登录的完整流程,包括用户认证、JWT Token的生成、权限处理(待实现)、用户信息的存储和登录成功的响应。
Spring Security和JWT结合使用的用户登录实现。
public ResponseResult login(User user)
:这是一个公共方法,返回类型是ResponseResult
,参数是一个User
对象。UsernamePasswordAuthenticationToken
对象,包含用户的账号和密码。authenticationManager.authenticate(authenticationToken)
进行认证,如果认证失败,则抛出SystemException
异常,异常中包含登录错误的枚举值。LoginUserDetails
对象,这是一个包含用户详细信息的Spring Security认证主体对象。LoginUserDetails
中获取用户ID,并使用JwtUtil.createJWT(id.toString())
方法生成一个JWT Token。//todo:权限
注释,表示这里应该添加权限相关的处理逻辑,但目前尚未实现。LoginUserDetails
对象存储到Redis缓存中,键为SystemConstants.LOGIN_USER_REDIS_KEY
加上用户ID。返回结果:
Map
对象来存储返回的数据。UserInfo
对象,包含用户的账号、姓名、头像和角色信息。UserInfo
对象放入Map
中。使用ResponseResult.okResult(map)
方法返回一个包含登录成功状态和数据的ResponseResult
对象。
@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);
}
POST /user/register
UserController
当客户端向/register
路径发送POST请求并包含用户信息(如用户名、密码等)时,这个方法会被调用,执行注册逻辑,并返回一个包含注册结果的ResponseResult
对象
@PostMapping("/register")
public ResponseResult resister(@RequestBody User user){
return userService.register(user);
}
UserServce
实现了用户注册的核心流程,包括检查账号是否存在、密码加密、生成唯一ID、保存用户信息到数据库,并返回注册成功的响应。
public ResponseResult register(User user)
:这是一个公共方法,返回类型是ResponseResult
,参数是一个User
对象。userAccountExist
方法检查传入的user
对象中的账号是否已经存在。SystemException
异常,异常中包含用户名已存在的枚举值。passwordEncoder.encode
方法对用户密码进行加密处理。user
对象的userPassword
属性。IdUtil.getSnowflake(1, 1).nextId()
方法生成一个唯一的ID(基于Snowflake算法)。user
对象的id
属性。save
方法将user
对象保存到数据库中。返回结果:
使用ResponseResult.okResult()
方法返回一个表示注册成功的ResponseResult
对象。
@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();
}
GET /user/getUserInfo
UserController
获取当前登录用户信息的接口,只有具有“用户”角色的用户才能访问
@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);
}
GET /user/page
Usercontroller
一个分页查询用户的接口,只有具有“管理员”角色的用户才能访问。它记录请求信息,执行分页查询,然后返回查询结果
@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
实现了一个分页查询用户信息的功能,它根据用户名、账号和角色进行过滤,并将查询结果转换为视图对象,最后返回一个包含分页信息的视图对象
public PageVO pageByUsernameAndUseraccount(...)
:这是一个公共方法,返回类型是PageVO
,参数包括当前页码currentPage
、页面大小pageSize
以及用于过滤的用户名userName
、账号userAccount
和角色userRole
。ArrayList
来存储UserVO
对象,这些对象将用于构建最终的分页视图对象。LambdaQueryWrapper
构建查询条件。这个查询包装器会根据传入的参数动态地添加查询条件。like
方法用于添加模糊匹配条件,eq
方法用于添加精确匹配条件。StringUtils.hasText
检查传入的字符串参数是否非空,只有非空时才会添加相应的查询条件。page
方法执行分页查询,传入分页参数和查询条件包装器。page
方法返回一个Page<User>
对象,包含了查询结果和分页信息。User
对象转换为UserVO
对象。BeanCopyUtil.copyBean
方法进行对象属性的复制。UserVO
对象的ID设置为字符串形式,以满足视图对象的要求。构建并返回分页视图对象:
PageVO
对象,传入用户视图对象列表和总记录数。返回PageVO
对象,包含了分页的用户信息和分页信息。
@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());
}
POST /user
UserController
一个添加新用户的接口,只有具有“管理员”角色的用户才能访问。
它接收用户信息,执行添加用户的操作,并返回操作成功的响应。
@PreAuthorize("@ps.hasRole('管理员')")
@PostMapping
@ResponseBody
public ResponseResult addUser(@RequestBody UserDTO userDTO){
userService.addUser(userDTO);
return ResponseResult.okResult();
}
UserServce
实现了将用户DTO转换为用户实体、加密密码、生成唯一ID,并保存用户信息到数据库的流程
public void addUser(UserDTO userDTO)
:这是一个公共方法,没有返回值(void
),参数是一个UserDTO
对象。BeanCopyUtil.copyBean
方法将UserDTO
对象的属性复制到一个新的User
对象中。这个工具方法通常用于将DTO(Data Transfer Object)对象的属性映射到实体对象。passwordEncoder.encode
方法对用户密码进行加密处理。user
对象的userPassword
属性。IdUtil.getSnowflake(1, 1).nextId()
方法生成一个唯一的ID(基于Snowflake算法)。user
对象的id
属性。保存用户:
调用save
方法将user
对象保存到数据库中。这个方法可能是一个Repository层的方法,负责实际的数据库操作。
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);
}
PUT /user
UserController
一个更新用户的接口,只有具有“管理员”角色的用户才能访问。
它接收用户信息,执行更新用户的操作,并返回操作成功的响应
@PreAuthorize("@ps.hasRole('管理员')")
@PutMapping
@ResponseBody
public ResponseResult updateUser(@RequestBody UserDTO userDTO){
User user = BeanCopyUtil.copyBean(userDTO, User.class);
userService.updateById(user);
return ResponseResult.okResult();
}
DELETE /user/{id}
UserController
一个删除用户的接口,只有具有“管理员”角色的用户才能访问。
它接收用户ID列表,执行删除用户的操作,并返回操作成功的响应
@PreAuthorize("@ps.hasRole('管理员')")
@DeleteMapping("/{ids}")
@ResponseBody
public ResponseResult deleteUserById(@PathVariable List<Long> ids){
userService.removeByIds(ids);
return ResponseResult.okResult();
}
完整的UserController
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
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认证过滤器
如果验证失败,则返回错误响应,阻止请求继续
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);
}
}
鉴权
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;
}
}
安全配置
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();
}
}
功能:用户的登陆和注册
元素
<template>
部分<div id="b-container" class="container b-container">
:定义了一个包含登录表单的容器。<form id="b-form" class="form" method="POST" action="">
:定义了一个登录表单,method="POST"
表示表单提交时将使用POST请求,action=""
属性为空,因为表单提交将通过JavaScript处理。<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="密码" />
:定义了两个输入框,分别用于输入用户名和密码。v-model
用于创建数据的双向绑定,v-model.lazy
用于在input
事件后更新数据。<a class="form__link">Forgot your password?</a>
:提供了一个忘记密码的链接。<button class="form__button button submit" @click.prevent="loginHandler()">SIGN IN</button>
:定义了一个提交按钮,@click.prevent
阻止了表单的默认提交行为,并调用了loginHandler
方法。<script setup>
部分ref
, watch
从vue
,useMainStore
从@/stores/message
,storeToRefs
从pinia
,ElMessage
从element-plus
,login
从@/common/api/user/index
,以及clearTokenAndUserInfo
, saveUserInfo
, setToken
从../common/utils/auth.js
。mainStore
实例,并使用storeToRefs
来响应式地监听showSignup
状态的变化。watch
来监听showSignup
的变化,并根据其值切换容器的CSS类。loginFrom
响应式对象,用于存储表单数据。loginHandler
方法,该方法执行登录操作:
clearTokenAndUserInfo
清除旧的认证信息。login
API方法,传入loginFrom.value
(包含用户名和密码)。res.code == 200
),则设置token,保存用户信息,并跳转到首页。router.push
用于在登录成功后导航到首页。<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>
注册表单
<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>
元素:
用户信息表格。
<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-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>
用户条件搜索,用户角色筛选。
<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>
用户账号、密码、名称、头像、角色的输入框。
<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>
保存和取消按钮。
##### 完整页面代码
<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>
用户管理模块提供了一套完整的用户信息管理功能,包括用户登录、注册、信息查看、编辑和删除。
通过严格的权限控制,确保了系统的安全性。前端页面友好、易用,提供了良好的用户体验。
数据分析和图表生成模块提供了完整的数据处理和可视化功能。
通过令牌桶限流和线程池优化,确保了接口的稳定性和高并发处理能力
同步与异步:用户可以通过ChartController上传数据文件并请求图表生成,支持同步和异步两种模式。
Controller
一个生成图表的接口,只有具有“用户”角色的用户才能访问。
它处理文件上传,执行限流检查,验证文件类型和大小,然后根据是否异步生成图表,并返回相应的结果
@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);
}
}
同步
实现了通过AI生成图表的完整流程,包括读取和验证上传的数据、构建AI请求、发送请求、解析响应、数据可视化处理和更新数据库
ChartDTO
复制数据到Chart
实体对象,并设置必要的属性,如ID、用户ID和状态(初始状态为“等待中”)。chartData
属性中,并将图表实体保存到数据库。AiUtil.doChat
方法发送构建好的请求数据到AI服务,并接收响应数据。generatedChartData
属性中。analysisConclusion
属性中。ChartVO
对象,以便将生成的图表数据和结论返回给前端
@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;
}
异步
创建线程池
这个配置类定义了一个自定义的线程池,包括核心线程数、最大线程数、工作队列、线程工厂和拒绝策略。这个线程池可以被应用程序中的其他组件使用,以异步执行任务
线程池参数配置:
corePoolSize
:核心线程池的大小为1,表示线程池中始终保持的线程数量。maximumPoolSize
:最大线程池的大小为2,表示线程池中允许的最大线程数量。keepAliveTime
:非核心线程空闲存活时间设置为100秒。unit
:时间单位设置为秒。workQueue
:工作队列使用ArrayBlockingQueue
,容量为5,用于存放待执行任务。threadFactory
:线程工厂,用于创建新线程。这里没有自定义线程名称或其他属性,直接使用默认的线程创建方式。
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请求、解析响应、数据可视化处理和更新数据库。通过异步处理,可以提高应用的性能,避免在生成图表时阻塞主线程。
public long generateChartByAIAsyn(ChartDTO chartDTO, MultipartFile file)
:这是一个公共方法,返回类型是long
,参数包括一个ChartDTO
对象和一个MultipartFile
对象,用于生成图表的异步操作。BeanCopyUtil.copyBean
方法将ChartDTO
对象的属性复制到一个新的Chart
实体对象中。Chart
实体对象中。IOException
,则打印堆栈跟踪。chartData
属性,并保存到数据库。threadPoolExecutor
来异步执行图表生成逻辑。@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
方法是非阻塞的,它会立即返回一个布尔值,指示是否成功获取到令牌
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进行数据分析
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;
}
}
### <template>
部分
el-row
和el-col
创建布局,分为三个主要部分:表单区域、分隔符和图表显示区域。el-form
创建一个表单,包含图表名称、分析目标、图表类型、异步分析选项和文件上传等字段。el-select
下拉选择框,异步分析使用el-switch
开关。el-upload
组件,限制上传文件大小不超过2MB,并且只能是.xls
或.xlsx
格式。div
元素作为图表的容器,并在底部提供了一个按钮用于获取分析结果。### <script>
部分
ElMessage
用于显示消息提示。submitForm
方法用于提交表单,根据是否异步分析调用不同的API,并处理响应结果。changeFile
方法用于更新表单中的文件数据。resetForm
方法用于重置表单。fileBeforeUpload
方法用于在文件上传前进行校验,确保文件大小和类型符合要求。chartRefresh
方法用于初始化或刷新图表。getChartResult
方法用于获取异步分析的结果,并更新图表和分析结论。
<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>
JWTAuthenticationTokenFilter:自定义过滤器,用于验证请求中的JWT。
异常处理:GlobalExceptionHandler处理认证和鉴权过程中的异常。
SystemException
异常和所有其他类型的Exception
异常。通过这种方式,可以统一异常处理逻辑,确保所有异常都能被恰当地记录和响应,提高应用程序的健壮性和用户体验。@RestControllerAdvice
:这个注解表示这个类是一个全局的异常处理控制器,它会对整个应用程序范围内的异常进行捕捉和处理。GlobalExceptionHandler
类:定义了一个全局异常处理器。private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
:创建一个日志对象,用于记录日志信息。@ExceptionHandler(SystemException.class)
:这个注解指定了方法systemException
用于处理SystemException
类型的异常。systemException(SystemException e)
方法:处理SystemException
异常。
log.error("发生错误:{}",e);
:记录错误日志,包括异常堆栈跟踪。return ResponseResult.errorResult(e.getCode(),e.getMsg());
:返回一个ResponseResult
对象,包含错误码和错误消息。@ExceptionHandler(Exception.class)
:这个注解指定了方法systemException
用于处理所有未被捕获的Exception
类型的异常。systemException(Exception e)
方法:处理通用Exception
异常。
log.error("发生错误:{}",e);
:记录错误日志,包括异常堆栈跟踪。return ResponseResult.errorResult(ResponseResult.AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());
:返回一个ResponseResult
对象,包含系统错误码和异常消息。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格式响应给客户端
AuthenticationEntryPointImpl
类:实现了AuthenticationEntryPoint
接口,用于自定义处理认证失败后的行为。commence
方法:当Spring Security认证失败时,这个方法会被调用。它接收HttpServletRequest
和HttpServletResponse
对象,以及一个AuthenticationException
异常对象。InsufficientAuthenticationException
:当认证信息不足时抛出的异常,例如,用户未提供认证信息。针对这种异常,返回一个特定的错误码NO_OPERATOR_AUTH
。InternalAuthenticationServiceException
:当认证过程中发生内部错误时抛出的异常,例如,密码错误。针对这种异常,返回一个错误码LOGIN_ERROR
。AuthenticationException
:对于其他类型的认证异常,返回系统错误码SYSTEM_ERROR
,并附带异常消息。WebUtils.renderString
方法将错误信息以JSON格式返回给客户端。这里使用JSON.toJSONString(responseResult)
将ResponseResult
对象转换为JSON字符串。e.printStackTrace();
:打印异常堆栈跟踪.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));
}
}
鉴权异常处理:
AccessDeniedHandlerImpl
类:实现了AccessDeniedHandler
接口,用于自定义处理用户访问被拒绝后的行为。handle
方法:当用户的访问被拒绝时,这个方法会被调用。它接收HttpServletRequest
和HttpServletResponse
对象,以及一个AccessDeniedException
异常对象。e.printStackTrace();
:打印异常堆栈跟踪,这通常用于调试目的。ResponseResult
对象,使用ResponseResult.errorResult
方法,并传入错误码NO_OPERATOR_AUTH
,表示用户没有操作权限。WebUtils.renderString
方法将ResponseResult
对象转换为JSON字符串,并写入到HttpServletResponse
中,作为HTTP响应返回给客户端。AccessDeniedException
:当用户尝试访问他们没有权限的资源时,Spring Security会抛出这个异常。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));
}
}