# BI APP
**Repository Path**: whiteinjava/bi-app
## Basic Information
- **Project Name**: BI APP
- **Description**: 智能BI平台
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 3
- **Forks**: 1
- **Created**: 2023-10-26
- **Last Updated**: 2025-01-09
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# BI智能平台说明文档
# 背景诉求
BI即所谓商业智能,是一个数据可视化的数据分析系统。市场上已有的主流BI平台,像帆软BI以及微软的Power BI。这个项目与上述主流的BI平台类似,不同于传统BI平台给使用人员带来的专业上的局限性;使用该BI平台能够通过输入诉求以及上传原始数据即可实现便捷的数据分析以及可视化。
## 1.整体需求分析
- 用户输入分析需求,上传原始数据,能够得到想要的诉求结果
- 用户、图表管理
- 由于API接口调用可能存在并发延迟,因此要求图表的生成能够实现异步化
- 后端服务对接AI
## 2.系统架构
### 2.1 基础架构

### 2.2 进阶架构

## 3.技术栈选型
### 3.1 前端
- React
- Umi + Ant Design Pro
- 可视化开发库 Echarts
- Umi openAPI后端代码生成
### 3.2 后端
- SpringBoot、SpringMVC
- MyBatis-Plus
- MySQL
- RabbitMQ
- Easy Excel 图表数据的上传解析
- Swagger + Knife4j 文档
- HuTool、Lombok
- openAPI接口开发
## 第一期 - 项目初始化
### 1. 前端Ant Design Pro初始化

### 2. 后端SpringBoot初始化

#### 2.1 Maven依赖项
```xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.7.2
com.shuai
BaiBI
0.0.1-SNAPSHOT
BaiBI
1.8
org.springframework.boot
spring-boot-starter-freemarker
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-aop
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.2
com.baomidou
mybatis-plus-boot-starter
3.5.2
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.session
spring-session-data-redis
org.springframework.boot
spring-boot-starter-data-elasticsearch
com.github.xiaoymin
knife4j-spring-boot-starter
3.0.3
com.qcloud
cos_api
5.6.89
org.apache.commons
commons-lang3
com.google.code.gson
gson
2.9.1
com.alibaba
easyexcel
3.1.1
cn.hutool
hutool-all
5.8.8
org.springframework.boot
spring-boot-devtools
runtime
true
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-configuration-processor
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
org.projectlombok
lombok
```
#### 2.2 SpringBoot配置文件
```yaml
spring:
application:
name: BaiBI # 项目名称
profiles:
active: dev # 默认开发环境
# 支持swagger3
mvc:
pathmatch:
matching-strategy: ant_path_matcher
session:
timeout: 2592000 # 配置session 30天过期
# 配置数据库链接
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/baixs_bi_db
username: root
password: 123456
servlet:
multipart:
max-file-size: 10MB # 配置上传文件的大小限制
server:
port: 8081
servlet:
context-path: /api
session:
cookie:
max-age: 2592000 # 设置cookie 30天过期
# MyBatis-Plus配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
```
#### 2.3 统一返回数据格式
```java
/**
* 通用返回类
*
* @param
* @author 程序员鱼皮
* @from 编程导航知识星球
*/
@Data
public class BaseResponse implements Serializable {
private int code;
private T data;
private String message;
public BaseResponse(int code, T data, String message) {
this.code = code;
this.data = data;
this.message = message;
}
public BaseResponse(int code, T data) {
this(code, data, "");
}
public BaseResponse(ErrorCode errorCode) {
this(errorCode.getCode(), null, errorCode.getMessage());
}
}
```
```java
/**
* 返回工具类
*
* @author 程序员鱼皮
* @from 编程导航知识星球
*/
public class ResultUtils {
/**
* 成功
*
* @param data
* @param
* @return
*/
public static BaseResponse success(T data) {
return new BaseResponse<>(0, data, "ok");
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse<>(errorCode);
}
/**
* 失败
*
* @param code
* @param message
* @return
*/
public static BaseResponse error(int code, String message) {
return new BaseResponse(code, null, message);
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode, String message) {
return new BaseResponse(errorCode.getCode(), null, message);
}
}
```
```java
/**
* 自定义错误码
*/
public enum ErrorCode {
SUCCESS(0, "ok"),
PARAMS_ERROR(40000, "请求参数错误"),
NOT_LOGIN_ERROR(40100, "未登录"),
NO_AUTH_ERROR(40101, "无权限"),
NOT_FOUND_ERROR(40400, "请求数据不存在"),
FORBIDDEN_ERROR(40300, "禁止访问"),
SYSTEM_ERROR(50000, "系统内部异常"),
OPERATION_ERROR(50001, "操作失败");
/**
* 状态码
*/
private final int code;
/**
* 信息
*/
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
```
#### 2.4 统一应用异常处理
```java
/**
* 自定义业务异常类
*/
public class BusinessException extends RuntimeException {
/**
* 错误码
*/
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}
public int getCode() {
return code;
}
}
```
```java
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public BaseResponse> businessExceptionHandler(BusinessException e) {
log.error("BusinessException", e);
return ResultUtils.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(RuntimeException.class)
public BaseResponse> runtimeExceptionHandler(RuntimeException e) {
log.error("RuntimeException", e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
}
}
```
#### 2.5 Mybatis-Plus配置
##### 2.5.1 分页插件配置
```java
@Configuration
@MapperScan("com.shuai.springbootinit.mapper")
public class MyBatisPlusConfig {
/**
* 拦截器配置
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
```

##### 2.5.2 日志输出与逻辑删除字段配置
```yaml
# MyBatis-Plus配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
```
#### 2.6 用户密码处理工具类
```java
/**
* 加密思路:
* 用户密码加盐的基本思路为:
* 随机生成一个去除分隔符的32位UUID;
* 将生成的32位UUID与用户明文密码拼接进行MD5加密;
* 将32位UUID与MD5加密后的(UUID + 用户密码)进行拼再添加一个分隔符,方便后续校验密码,将其存储到数据库中
* 最终的密文密码格式为:生成的32位UUID + # + MD5(生成的32位UUID + 用户明文密码)
*/
/**
* 解密思路:
* 先从数据库中根据用户名获得用户信息
* 将用户加密后的密码的前32位UUID取出
* 将取出的32位UUID与用户输入的密码拼接并进行MD5加密
* 将32位UUID与MD5加密后的32位UUID和用户明文密码进行拼接
* 比较按照用户明文密码加密后的密文和数据库中存储的密文是否相同
*/
public abstract class PasswordUtil {
private static final String SEPARATOR = "#";
//加密
public static String encryptPwd(String password) {
String frontMatter = UUID.randomUUID().toString().replace("-", ""); //生成32位的一个前置校验
String endMatter = DigestUtils.md5DigestAsHex((frontMatter + password).getBytes()); // 将明文密码和前置校验拼接进行md5加密
return frontMatter + SEPARATOR + endMatter; // 将前置校验加上一个$加上前置校验和明文密码加在一起加密后的后置校验共65位
}
private static String getExpectPwd(String frontMatter, String originalPwd) {
return frontMatter + SEPARATOR + DigestUtils.md5DigestAsHex((frontMatter + originalPwd).getBytes());
}
//校验
public static boolean check(String encryptedPwd, String originalPwd) {
//取得加密后密码的前32位
String frontMatter = encryptedPwd.split(SEPARATOR)[0];
String expectPwd = getExpectPwd(frontMatter, originalPwd);
return encryptedPwd.equals(expectPwd);
}
}
```
### 3.库表设计
```sql
drop database if exists baixs_bi_db;
-- 创建库
create database if not exists baixs_bi_db;
-- 切换库
use baixs_bi_db;
-- 用户表
drop table if exists user;
create table if not exists user
(
id bigint auto_increment comment 'id' primary key,
userAccount varchar(256) not null comment '账号',
userPassword varchar(512) not null comment '密码',
userName varchar(256) null comment '用户昵称',
userAvatar varchar(1024) null comment '用户头像',
userRole varchar(256) default 'user' not null comment '用户角色:user/admin',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除',
index idx_unionId (userAccount)
) comment '用户' collate = utf8mb4_unicode_ci;
-- 图表信息表
drop table if exists chart;
create table if not exists chart
(
id bigint auto_increment comment 'id' primary key,
`name` varchar(128) null comment '图标名称';
goal text null comment '分析目标',
chartType varchar(128) null comment '图表类型',
chartData text null comment '图表数据',
genChartData text null comment '生成的图表数据',
genChartResult text null comment '生成的分析结论',
userId bigint not null comment '创建用户 id',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除'
) comment '帖子' collate = utf8mb4_unicode_ci;
```
---
## 第二期 - 用户管理和前后端联调测试
### 接口处理逻辑
### 1. 用户注册
```java
@PostMapping("/register")
public BaseResponse register(@RequestBody UserRegisterRequest userRegisterRequest) {
if (userRegisterRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String userAccount = userRegisterRequest.getUserAccount();
String checkPassword = userRegisterRequest.getCheckPassword();
String userPassword = userRegisterRequest.getUserPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 检验输入密码和校验密码是否一致
if (!StringUtils.equals(userPassword, checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码和校验密码不一致!");
}
// 对用户输入的密码进行加密
User user = new User();
String encryptedPwd = PasswordUtil.encryptPwd(userPassword);
BeanUtils.copyProperties(userRegisterRequest, user);
user.setUserPassword(encryptedPwd);
// 保存注册用户
synchronized (lock) {
// 校验数据库中是否已经有该用户名
LambdaQueryWrapper userQueryWrapper = new LambdaQueryWrapper<>();
userQueryWrapper.eq(User::getUserAccount, userAccount);
if (userService.count(userQueryWrapper) > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名已存在");
}
// 保存用户
userService.save(user);
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
return ResultUtils.success(userVO);
}
```
```json
# 请求参数
{
"checkPassword": "12345678",
"userAccount": "root",
"userPassword": "12345678"
}
# 响应参数
{
"code": 0,
"data": {
"id": "1717523771382636546",
"userAccount": "root",
"userName": null,
"userAvatar": null,
"userRole": null,
"createTime": null,
"updateTime": null,
"isDelete": null
},
"message": "ok"
}
```
### 2. 用户登录
```java
@PostMapping("/login")
public BaseResponse login(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null
|| StringUtils.isAllBlank(userLoginRequest.getUserAccount(), userLoginRequest.getUserPassword())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 根据用户名从数据库中查询用户信息
LambdaQueryWrapper userQueryWrapper = new LambdaQueryWrapper<>();
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserPassword();
userQueryWrapper.eq(User::getUserAccount, userAccount);
User user = userService.getOne(userQueryWrapper);
if (user == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在!");
}
if (!PasswordUtil.check(user.getUserPassword(), userPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名或密码错误!");
}
// 存储用户会话对象
request.getSession().setAttribute(UserConstant.LOGIN_STATE_KEY, user.getId());
// 返回登录成功信息
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
return ResultUtils.success(userVO);
}
```
```json
# 请求参数
{
"userAccount": "root",
"userPassword": "12345678"
}
# 响应参数
{
"code": 0,
"data": {
"id": "1717523771382636546",
"userAccount": "root",
"userName": null,
"userAvatar": null,
"userRole": "user",
"createTime": "2023-10-26T12:49:11.000+00:00",
"updateTime": "2023-10-26T12:49:11.000+00:00",
"isDelete": 0
},
"message": "ok"
}
```
### 3.联调用户登录前后端
```jsx
openAPI: [
{
requestLibPath: "import { request } from '@umijs/max'",
// 或者使用在线的版本
// schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json",
schemaPath: "http://localhost:8081/api/v2/api-docs",
// schemaPath: join(__dirname, 'oneapi.json'),
projectName: 'baiBI_front',
mock: false,
},
],
```

```jsx
/**
* 在用户登录页面发送登录请求,测试前后端联调效果
*/
useEffect(() => {
loginUsingPOST({}).then(res => {
console.log(res)
})
})
```

## 第三期 - 智能AI分析
### 需求分析
1. 后端校验上传参数正确性,并进行AI接口调用的成本控制(次数统计限制、用户鉴权)
2. 由于openAPI接口调用字数的限制以及准确性问题,手动优化用户分析需求
3. 将用户上传的excel格式的文件转换为csv格式
4. 根据用户提问,创建用户提词
### excel转csv
```java
/**
* 将用户上传的excel文件转换成字符串
*/
@Slf4j
public class ExcelHandleUtils {
public static String handleExcelToStr(MultipartFile file) {
List