Memory BI 智能分析平台-开发文档

本文最后更新于:7 个月前

day1

前端框架搭建

Ant Design Pro前端框架初始化

  • 详情可见:《》

后端框架搭建

  • 修改数据库配置,修改启动端口,开启接口文档,测试登陆注册功能

day2

建立数据库表

  • 图表信息表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
create table if not exists chart
(
id bigint auto_increment comment 'id' primary key,
`name` varchar(128) null comment '图表名称',
goal text null comment '分析目标',
chartData text null comment '图表数据',
chartType varchar(128) null comment '图表类型',
genChart text null comment '生成的图表数据',
genResult text null comment '生成的分析结论',
userId bigint null comment '创建用户 id',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP not null comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除'

) comment '图表信息表' collate = utf8mb4_unicode_ci;

分析Excel文件,获取数据

  • 简单地测试一把:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
File file = null;
try {
file = ResourceUtils.getFile("classpath:test_excel.xlsx");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 读取数据
List<Map<Integer, String>> list = null;
try {
list = EasyExcel.read(multipartFile.getInputStream())
.excelType(ExcelTypeEnum.XLSX)
.sheet()
.headRowNumber(0)
.doReadSync();
} catch (IOException e) {
throw new RuntimeException(e);
}
if (CollUtil.isEmpty(list)) {
return "";
}

学习 BI 智能分析平台,让我更加拓宽了学习思路:

  • 最近的Ant Design Pro前端框架初始化

  • 系统领受了如何调用 AI 完成:根据用户提问,自动生成图表以及分析结论,这其中有很多值得学习的新知识:

    • 提问 AI 的技巧(四个技巧)
    • 如何在调用现有的 OpenAPI接口?(三种主流方法)
  • 实现AI生成图表业务的基本流程:(2023/10/05晚)
    • 构造用户请求
    • 调用鱼聪明 sdk 得到 AI 响应结果(指定生成内容的固定格式,如 Echarts 的图表代码格式)
    • 从响应结果中,取出需要的信息
    • 保存图表到数据库,返回给前端进行展示
  • 理解了 API 开放平台带来的便利,对其架构设计有了更深的理解

  • 自定义 AI 助手,做好预设,能让生成结果更加准确、符合预期

day3

构造用户请求

  • 我们需要对 AI 进行提问,让它生成符合我们预期的数据
  • 这里我们提下 提问 AI 的四个技巧

提词技巧

  • 持续输入,持续优化(2023/10/05早)
  • 考虑到输入限制,进行数据压缩,把很长的内容提取关键词(也可以让AI完成)
  • 做预设:在系统层面做预设,一般要比在提问关键词中做预设效果要好
  • 除系统预设外,额外添加两条一问一答形式,相当于给AI提示

自定义预设

  • 做好预设,能让生成结果更加准确、符合预期

  • 我在鱼聪明平台上,自定义了第一个 AI 助手,能够专门回答固定领域的问题:
  • 我们对 自定义的AI小助手做好预设:

image-20231006195531671

image-20231006195551555

  • 如上,我们对该 AI 助手做好如上预设
  • 实测结果令我非常满意:(2023/10/05晚)

image-20231005210141145

数据分析助手预设

image-20231006200036142

你是一个数据分析师和前端开发专家,接下来我会按照以下固定格式给你提供内容:
分析需求:
{数据分析的需求或目标}
原始数据:
{csv格式的原始数据,用,作为分隔符}
请根据这两部分内容,按照以下指定格式生成内容(此外不要输出任何多余的开头、结尾、注释)
【【【【【
{前端 Echarts V5 的 option 配置对象 js 代码,合理地将数据进行可视化,不要生成任何多余的内容,比如注释}
【【【【【
{明确的数据分析结论,越详细越好,不要生成多余的注释}

  • 实测效果如下:

image-20231006200131460

1
2
3
4
5
6
7
8
9
10
11
12
13
{
xAxis: {
type: 'category',
data: ['1号', '2号', '3号', '4号']
},
yAxis: {
type: 'value'
},
series: [{
type: 'line',
data: [10, 20, 35, 17]
}]
}
  • 将生成的代码粘贴至 Echarts 示例页面下,图表生成正常,效果符合预期:

image-20231006200502519

  • 总之,提问 AI 的要点有三:做好预设限制输出格式指定示例问答

调用 AI 的3种方式

直接调用官方接口
使用云服务商提供的封装接口
鱼聪明 AI 接口开放平台

调用 AI 尝试

  • 我们按照 sdk 的使用文档,写出如下示例代码:
1
2
3
4
5
<dependency>
<groupId>com.yucongming</groupId>
<artifactId>yucongming-java-sdk</artifactId>
<version>0.0.3</version>
</dependency>
1
2
3
4
yuapi:
client:
access-key: ****************
secret-key: ****************
1
2
3
4
5
6
7
8
9
@Resource
private YuCongMingClient client;

DevChatRequest devChatRequest = new DevChatRequest();
devChatRequest.setModelId(1709914748273168386L);
devChatRequest.setMessage("我想介绍下我的新游戏,它是一种双人棋牌游戏");

BaseResponse<DevChatResponse> response = client.doChat(devChatRequest);
System.out.println(response.getData());

image-20231006201523133

  • 调用成功
  • 这里不得不夸赞一句,鱼总写的 AI 开放平台 sdk 使用文档,简单明了,使用方便,赞!(2023/10/06晚)

调用AI,完成数据分析

  • 着手完成这个功能,要考虑的还有很多:

    • 用户发起AI调用传递参数:图表名、分析需求、原始数据,并一一对这些参数进行合适的校验
    • 对用户的请求信息进行分析,组合成对话信息,远程调用 AI
    • 发起调用,指定 AI id、对话信息,等待返回结果
    • 封装返回结果:调用人、图表信息等,在前端页面进行展示
    • 生成的图表信息还应该存入数据库中持久化
  • 基本完成
  • 数据分析基本完成

1
2
3
4
5
6
7
8
9
10
// 3.分析Excel图表,获取原始数据
String excelToCsv = ExcelUtils.excelToCsv(multipartFile);
userInput.append("\n")
.append("分析需求:").append("\n")
.append(goal).append(", ").append("请生成一张").append(chartType).append("\n")
.append("原始数据:").append("\n")
.append(excelToCsv);

// 4.执行AI接口调用
String result = aiManager.doChat(AiConstant.BI_MODEL_ID, userInput.toString());
  • 前端智能分析页面基本完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const res = await genChartByAiUsingPOST(params, {}, values.file.file.originFileObj);
if (!res?.data) {
message.error('分析失败');
}
message.success('分析成功');
const chartOption = JSON.parse(res?.data?.genChart ?? '');
// const chartOption = res.data.genChart
if (!chartOption) {
throw new Error('图表代码解析错误')
} else {
// 图表信息
setChart(res.data);
setOption(chartOption);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<Col span={12}>
<Card title="分析结论">
{chart?.genResult ?? <div>请先在左侧进行提交</div>}
<Spin spinning={submitting}/>
</Card>
<Divider/>
<Card title="可视化图表">
{
option ? <ReactECharts option={option}/> : <div>请先在左侧进行提交</div>
}
<Spin spinning={submitting}/>
</Card>
</Col>
  • 效果如下:(2023/10/08午)

image-20231008152044907

day4

  • 优化后端逻辑,返回图表列表(2023/10/08晚)
  • 再次调试智能分析功能,正常返回:

image-20231008213944416

图表列表页面

  • 快速编写请求,测试响应结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const loadData = async () => {
try {
const res = await listChartByPageUsingPOST(searchParams);
if (res.data) {
// 获取图表列表
setChartList(res.data?.records ?? []);
// 获取总数
setTotal(res.data?.total ?? 0);
} else {
message.error("获取我的图标失败")
}
} catch (e: any) {
message.error("获取我的图标失败" + e.message)
}
}
  • 监听 searchParams 变化,改变查询参数即可自动查询(比如切换页数,current改变
1
2
3
useEffect(() => {
loadData();
}, [searchParams]);

image-20231008220655471

  • 简单的样式,效果如下:

  • 这里由于前几条Chart数据,不是JSON代码,导致前端 ECharts 渲染失败:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
xAxis: {
type: 'category',
data: ['1号', '2号', '3号', '4号', '5号', '6号', '7号']
},
yAxis: {
type: 'value'
},
series: [{
data: [10, 20, 30, 90, 0, 10, 20],
type: 'bar'
}]
}

image-20231008222510291

  • 图表分页、添加搜索框、添加 loading 效果,根据 name 查询图表基本完成(2023/10/08晚)

day5

系统优化

文件校验

  • 我智能分析支持用户上传文件,如果用户上传一个超大的文件怎么办?比如 1000 G

    • 只要涉及到用户自主上传操作,一定要校验文件(图像)
    • 校验:
      • 文件大小
      • 文件后缀
      • 文件内容(成本略高)
      • 文件合规(敏感内容)
  • 校验文件大小和文件后缀(2023/10/11)
1
2
3
4
5
6
7
8
9
10
/**
* 允许上传的文件大小
*/
long ONE_MB = 1024 * 1024L;


/**
* 合法的文件后缀
*/
List<String> VALID_FILE_SUFFIX_LIST = Arrays.asList("xlsx", "xls");
1
2
3
4
5
6
7
8
9
// 3.1.校验文件
// 3.1.1.校验文件大小
long size = multipartFile.getSize();
ThrowUtils.throwIf(size > ONE_MB, ErrorCode.PARAMS_ERROR, "文件超过 1M");

// 3.1.2.校验文件后缀
String originalFilename = multipartFile.getOriginalFilename();
String suffix = FileUtil.getSuffix(originalFilename);
ThrowUtils.throwIf(!VALID_FILE_SUFFIX_LIST.contains(suffix), ErrorCode.PARAMS_ERROR, "文件后缀非法");

分库分表

限流

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.21.0</version>
</dependency>
  • 做好相关配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissionConfig {
private String host;

private String port;

private String password;

private Integer database;

@Bean
public RedissonClient redissonClient() {
// 1. 创建配置
Config config = new Config();
String redisAddress = String.format("redis://%s:%s", host, port);
// 使用单个Redis,没有开集群 useClusterServers() 设置地址和使用库
config.useSingleServer().setAddress(redisAddress).setDatabase(database).setPassword(password);
// 2. 创建实例
return Redisson.create(config);
}
}
1
2
3
4
redis:
port: 6379
host: localhost
database: 1
  • 限流实现(区别不同的限流器,每个用户都分别拥有对应的限流器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 限流实现
*
* @param key 识别用户的key
*/
public void doRateLimit(String key) {
// 创建限流器
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
// 每当一个操作来了之后,请求一个令牌
boolean canOp = rateLimiter.tryAcquire(1);
// 超出发放令牌数目,请求过于频繁
ThrowUtils.throwIf(!canOp, ErrorCode.TOO_MANY_REQUEST);
}
  • 简单测试,效果良好,测试结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void doRateLimit() throws InterruptedException {
String userId = "1";
for (int i = 0; i < 2; i++) {
redisLimiterManager.doRateLimit(userId);
System.out.println("成功");
}
Thread.sleep(1000);
for (int i = 0; i < 5; i++) {
redisLimiterManager.doRateLimit(userId);
System.out.println("成功");
}
}

image-20231011180516219

  • 业务中使用限流器,实现调用智能分析接口时,限制单位时间内的调用次数
1
2
// 限流(限制用户的调用次数,以用户id为key,区分各个限流器)
redisLimiterManager.doRateLimit("genCharByAi_" + loginUser.getId());

day6

系统问题分析

  • 问题场景:调用的服务处理能力有限,或者接口的处理时间 / 返回时间较长,就应该考虑异步化了
  1. 用户等待的时间比较长:等待 AI 生成图表,AI 处理过程比较长
  2. 业务服务器的处理能力有限,短时间内只能处理一个请求,请求过多,资源紧张导致 AI 处理不过来,严重时会导致服务器宕机,无法处理新的请求
  • 所以我们要实现 AI 生成图表异步化 (2023/10/15晚)

异步化

  • 同步:一件事做完,再做另一件事
  • 异步:第一件事不用做完,就可以开始做第二件事,等第一件事完成后,通知这件事做好了,进行后续处理就行

异步化后的业务流程

  • 简单回顾异步化前的业务流程:
    • 用户填写表单,提交生成图表请求,在此页面等待结果返回
    • 服务器校验请求信息,调用第三方 AI 服务处理图表生成请求并返回响应结果,实现图表智能分析
    • 可以发现,在用户提交图表分析请求后,需持续等待服务器处理,直到服务器返回处理结果

image-20231016211833171

  • 异步化后的业务流程:
    • 用户提交生成图表请求
    • 服务器接受请求,立刻把图表信息保存在数据库中(作为一个任务)
    • 用户可以在图表管理页面查看所有图表的信息和图表状态(已生成的、生成中的、生成失败的)
    • 用户可以修改生成失败的图表信息,点击重新生成

image-20231016211813595

线程池

线程池的实现

线程池的参数

线程池的工作机制

  • 这部分内容我计划在新的博文中介绍,可以看这篇文章学习:

🥣 推荐阅读:

简单测试,理解线程池工作流程

自定义线程池

  • 自定义线程池:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 自定义线程池
*/
@Configuration
public class ThreadPoolExecutorConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor() {
ThreadFactory threadFactory = new ThreadFactory() {
// 记录线程数
private int count = 1;
@Override
public Thread newThread(@NotNull Runnable r) {
Thread thread = new Thread(r);
thread.setName("线程" + count);
count++;
return thread;
}
};
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
(2, 4, 100,
TimeUnit.MINUTES, new ArrayBlockingQueue<>(4), threadFactory);
return threadPoolExecutor;
}
}
  • 在代码中,首先定义了一个ThreadFactory接口的实现类,用于创建线程(2023/10/15晚)
  • newThread方法中,通过创建线程并设置名称的方式来实现线程的创建。每次创建线程时,都会使用一个计数器count,用于记录线程的数量,保证线程名称的唯一性
  • 然后,使用ThreadPoolExecutor类创建一个线程池对象。括号内的参数依次为:核心线程数、最大线程数、线程空闲时间、时间单位、任务队列以及线程工厂。这些参数分别表示线程池的基本配置,如最小/最大线程数、线程空闲时间等。其中,任务队列使用了ArrayBlockingQueue,表示使用有界队列来存储线程任务
  • 最后,将创建好的线程池对象返回,供其他地方进行调用和使用。

提交任务到线程池

  • 提交任务到线程池:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 任务队列测试 提交任务到线程池
*/
@RestController
@RequestMapping("/queue")
@Slf4j
public class QueueController {
@Resource
private ThreadPoolExecutor threadPoolExecutor;

@GetMapping("/get")
public String get() {
HashMap<String, Object> map = new HashMap<>();
int size = threadPoolExecutor.getQueue().size();
map.put("队列长度", size);
long taskCount = threadPoolExecutor.getTaskCount();
map.put("任务总数", taskCount);
long completedTaskCount = threadPoolExecutor.getCompletedTaskCount();
map.put("已完成任务数", completedTaskCount);
int activeCount = threadPoolExecutor.getActiveCount();
map.put("正在工作的线程数", activeCount);
return JSONUtil.toJsonStr(map);
}

@GetMapping("/add")
public void add(String name) {
CompletableFuture.runAsync(() -> {
log.info(Thread.currentThread().getName() + "正在执行中");
try {
Thread.sleep(600000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, threadPoolExecutor);
}
}
  • 这段代码是一个简单的示例,在Spring Boot中使用自定义的线程池。首先通过@Resource注解将之前配置好的线程池对象threadPoolExecutor注入进来,然后定义了两个接口方法:get和add

  • 在get方法中,通过调用线程池对象的不同方法,获取了线程池的一些状态信息,如队列长度、任务总数、已完成任务数和正在工作的线程数,并将这些信息封装进一个HashMap中,最后使用 JSONUtil.toJsonStr 方法将其转换成JSON格式的字符串返回。

  • 在add方法中,使用 CompletableFuture.runAsync 方法来在线程池中执行一个任务。这里的任务是一个简单的代码块,通过log输出当前线程的名称,并休眠10分钟。通过将线程池对象threadPoolExecutor作为参数传递给runAsync方法,使得任务在该线程池中执行。

  • 通过这段代码,我们可以在Spring Boot项目中方便地使用自定义的线程池,并对其进行状态监控和管理(2023/10/15晚)

  • 由 ThreadPoolExecutorConfig 配置可知,我们自定义的线程池参数设置如下:

    • 核心线程数:2
    • 最大线程数:4
    • 任务队列数:4
    • 超时等待时间:未设置,默认拒绝
    • 时间单位:秒(SECEND)
  • 我们开始测试,依次添加任务,观察执行的线程数和任务队列数,详情如下:

image-20231015192743819

image-20231015192927650

image-20231015193033848

image-20231015193106483

image-20231015193134223

  • 简单描述一下测试情况吧:

    • 正在运行的线程数未达到核心线程数阈值时,优先添加线程处理新任务
    • 正在运行的线程数达到核心线程数阈值,但任务队列未满时,优先将任务放入任务队列中
    • 任务队列放满后,但正在运行的线程数未达到最大线程数阈值时,优先添加线程处理新任务
    • 正在运行的线程数达到最大线程数阈值后,采用合适的拒绝策略(这里我们采用默认的拒绝策略:直接扔掉这个任务)
  • 测试完成(2023/10/15晚)

开发实现

库表设计

  • 给 chart 表新增字段
    • 任务状态字段(排队中、执行中、已完成、执行失败)
    • 任务执行字段(记录任务执行过程中的一些信息,比如为何失败)
1
2
status  varchar(128) not null default 'wait' comment 'wait,running,succeed,failed',
execMessage text null comment '执行信息',

流程梳理

  • 用户发起智能分析请求后,先把生成的图表信息立刻保存到数据库中,然后提交该请求到任务队列

  • 任务实现:先修改图表任务状态为“执行中”

    • 待执行完成后,修改为“已完成”、并保存执行结果
    • 若执行失败,状态修改为“失败”,记录任务失败信息
  • 用户可以在图表管理界面查看所有图表(包括已生成的、生成中的、生成失败的)的信息和状态

  • 流程大致实现,明天再做优化,把该写的 todo 都完成了(2023/10/15晚)

异步化实现

  • 详细的业务代码在这里不作展示,可在代码仓库查看
  • 我们对照异步化后的业务流程图,简单梳理下代码逻辑:

image-20231016211908520

前端优化

  • 图表列表可根据图表状态进行展示

image-20231016211241966

image-20231016211150999

TODO

  • 美观的首页

  • 图表生成后,提供友好的提示:提交请求后、图表生成后(成功、失败、忙碌中)

  • 枚举图表状态

Memory BI 智能分析平台-开发文档
http://example.com/2023/10/03/Memory BI智能分析平台-开发文档/
作者
Memory
发布于
2023年10月3日
更新于
2023年10月15日
许可协议