BI 智能分析业务
智能分析业务
需求分析
业务流程分析
【1】用户输入
- 分析目标
- 上传原始数据(excel)
- 更精细化地控制图表:比如图表类型、图表名称等
【2】后端校验
- 校验用户的输入否合法(此如长度)
- 成本控制(次数统计和校验、鉴权等)
【3】把处理后的数据输入给Al模型(调用Al接口),让AI模型给系统提供图表信息、结论文本
【4】图表信息(是一段json配置、是一段代码)、结论文本在前端进行展示
接口开发
根据用户的输入(文本和文件),最后返回图表信息和结论文本
需要在接收用户上传的excel文件时,在请求的参数中对接文件,能够接收到文件(可以仿照模板中文件上传FileController的接口进行编写)
将model/dto/file/UploadFileRequest复制到model/dto/chart/重命名为GenChartByAiRequest.java,此处GenChartByAiRequest字段设计可以参考图表信息表
/**
* 文件上传请求
*/
@Data
public class GenChartByAiRequest implements Serializable {
/**
* 名称
*/
private String name;
/**
* 分析目标
*/
private String goal;
/**
* 图表类型
*/
private String chartType;
private static final long serialVersionUID = 1L;
}
原始数据压缩
ChartController文件上传分析
/**
* 智能分析(同步)
*
* @param multipartFile
* @param genChartByAiRequest
* @param request
* @return
*/
@PostMapping("/upload")
public BaseResponse<String> genChartByAi(@RequestPart("file") MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
String name = genChartByAiRequest.getName();
String goal = genChartByAiRequest.getGoal();
String chartType = genChartByAiRequest.getChartType();
// 校验
ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空");
ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100, ErrorCode.PARAMS_ERROR, "名称过长");
// 读取用户上传的excel文件,进行处理
User loginUser = userService.getLoginUser(request);
// 文件目录:根据业务、用户划分
String uuid = RandomStringUtils.randomAlphanumeric(8);
String filename = uuid + "-" + multipartFile.getOriginalFilename();
File file = null;
try {
// 返回可访问地址
return ResultUtils.success("");
} catch (Exception e) {
// log.error("file upload error, filepath = " + filepath, e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");
} finally {
if (file != null) {
// 删除临时文件
boolean delete = file.delete();
if (!delete) {
// log.error("file delete error, filepath = {}", filepath);
}
}
}
}
读取到excel文件,需要进行处理。AI接口普遍都有输入字数限制,因此要尽可能压缩数据,使得允许多传一些数据。ExcelUtils设计如下(读取excel文件需要转化为csv:参考ExcelUtils实现)
/**
* Excel 相关工具类
*/
@Slf4j
public class ExcelUtils {
/**
* excel 转 csv
*
* @param multipartFile
* @return
*/
public static String excelToCsv(MultipartFile multipartFile) {
// File file = null;
// try {
// file = ResourceUtils.getFile("classpath:网站数据.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) {
log.error("表格处理错误", e);
}
if (CollUtil.isEmpty(list)) {
return "";
}
// 转换为 csv
StringBuilder stringBuilder = new StringBuilder();
// 读取表头
LinkedHashMap<Integer, String> headerMap = (LinkedHashMap) list.get(0);
List<String> headerList = headerMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList());
stringBuilder.append(StringUtils.join(headerList, ",")).append("\n");
// 读取数据
for (int i = 1; i < list.size(); i++) {
LinkedHashMap<Integer, String> dataMap = (LinkedHashMap) list.get(i);
List<String> dataList = dataMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList());
stringBuilder.append(StringUtils.join(dataList, ",")).append("\n");
}
return stringBuilder.toString();
}
public static void main(String[] args) {
excelToCsv(null);
}
}
文件上传压缩接口测试
@PostMapping("/upload")
public BaseResponse<String> genChartByAi(@RequestPart("file") MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
String name = genChartByAiRequest.getName();
String goal = genChartByAiRequest.getGoal();
String chartType = genChartByAiRequest.getChartType();
// 校验
ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空");
ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100, ErrorCode.PARAMS_ERROR, "名称过长");
// 测试读取文件
String result = ExcelUtils.excelToCsv(multipartFile);
return ResultUtils.success(result);
}
本地测试:启动项目访问接口文档测试
AI调用
如何使用AI生成结论、图表?
将数据给到AI,然后拼接用户的分析目标
输入:
系统预设(提前告诉他职责、功能回复格式要求) +分析目标+压缩后的数据
最简单的系统预设:
你是一个数据分析师,接下来我会给你我的分析目标和原始数据,请告诉我分析结论。
AI提词技巧3:
在系统(模型)层面做预设效果一般来说,蚍直接拼接在用户消息里效果更好一些。
AI提词技巧4:
除了系统预设外,额外关联一问一答两条消息,相当于给A1一个提示。
参考AI接口,此处将预设文本传入,查看AI响应效果
将excel解析的数据给AI,然后拼接用户的分析目标。将系统预设目标以文本方式拼接,然后调用AI
调整ChartController实现压缩文本拼接,将拼接文本用于后续和AI接口对接
/**
* 智能分析(同步)
*
* @param multipartFile
* @param genChartByAiRequest
* @param request
* @return
*/
@PostMapping("/upload")
public BaseResponse<String> genChartByAi(@RequestPart("file") MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
String name = genChartByAiRequest.getName();
String goal = genChartByAiRequest.getGoal();
String chartType = genChartByAiRequest.getChartType();
// 校验
ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空");
ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100, ErrorCode.PARAMS_ERROR, "名称过长");
// 用户输入
StringBuilder userInput = new StringBuilder();
userInput.append("你是一个数据分析师,接下来我会给你我的分析目标和原始数据,请告诉我分析结论。");
userInput.append("分析目标:").append(goal).append("\n");
// 压缩后的数据
String result = ExcelUtils.excelToCsv(multipartFile);
userInput.append("数据:").append(result).append("\n");
return ResultUtils.success(userInput.toString());
}
启动后台访问接口文档,测试响应结果如下
{
"code": 0,
"data": "你是一个数据分析师,接下来我会给你我的分析目标和原始数据,请告诉我分析结论。分析目标:分析网站用户\n数据:日期,用户数\n1号,10\n2号,20\n3号,30\n4号,90\n5号,0\n6号,10\n7号,20\n\n",
"message": "ok"
}
AI预设问答效果:
后端开发
1.三种AI数据分析应用方式
AI生成结论
示例问法:
你是一个数据分析师,请帮我分析网站用户的增长趋势
原始数据如下:
日期,用户数
1号,10
2号,20
3号,30
AI生成图表
AI虽然无法生成现成的图表,但是可以生成图表代码,利用前端组件库(Echarts)将生成代码进行渲染展示(Echarts在线展示)
预期生成代码参考:
option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [150, 230, 224, 218, 135, 147, 260],
type: 'line'
}
]
};
AI提问技巧
提问技巧参考
为了让AI更好地理解输入,给出预期精确的输出,需要严格控制提问词
【1】使用系统预设+控制输入格式(便于AI精确理解需求)
- 助手设定
你是一个数据分析师和前端开发专家,接下来我会按照以下固定格式给你提供内容:
分析需求:
{数据分析的需求或者目标}
原始数据:
{csv格式的原始数据,用,作为分隔符}
请根据以上内容,帮我生成数据分析结论和可视化图表代码
- 用户提问
分析需求:
分析网站用户的增长情况
原始数据:
日期,用户数
1号,10
2号,20
3号,30
【2】控制输出格式(便于AI返回内容能够更好地为系统所用)
- Prompt预设
你是一个数据分析师和前端开发专家,接下来我会按照以下固定格式给你提供内容:
分析需求:
{数据分析的需求或者目标}
原始数据:
{csv格式的原始数据,用,作为分隔符}
请根据这两部分内容,按照以下指定格式生成内容(此外不要输出任何多余的开头、结尾、注释)
【【【【【
{前端Echarts V5的option 配置对象js代码,合理地将数据进行可视化,不要生成任何多余的内容,比如注释
【【【【【
{明确的数据分析结论、越详细越好,不要生成多余的注释}
- 生成内容
【【【【【
{
title: {
text: '网站用户增 长情况',
subtext :
},
tooltip: {
trigger: ' axis',
axisPointer: {
type:‘shadow
},
legend: {
data: ['用户数']
xAxis: {
data: ['1号,'2号,,'3号']
},
yAxis: {},
series: [{
name :
'用户数',
type: 'bar',
data: [10, 20, 30]
}]
}
【【【【【
根据数据分析可得,该网站用户数量逐日增长,时间越长,用户数量增长越多。
- 指定一个示例问答,one-shot或者few-shot
one-shot:给AI一轮示例问答
few-shot:给AI多轮示例问答
AI助手提问示例
创建AI助手
创建完成,可在【我的助手】模块查看助手信息
【1】点击AI助手,对话框输入【用户提问】
分析需求:
分析网站用户的增长情况
原始数据:
日期,用户数
1号,10
2号,20
3号,30
【2】控制输出格式
编辑助手信息,修改为【2】中的Prompt预设
还是问同一个问题,确认问答信息
【【【【【
{
"title": {
"text": "用户增长情况"
},
"tooltip": {
"trigger": "axis"
},
"legend": {
"data": ["用户数"]
},
"grid": {
"left": "3%",
"right": "4%",
"bottom": "3%",
"containLabel": true
},
"toolbox": {
"feature": {
"saveAsImage": {}
}
},
"xAxis": {
"type": "category",
"boundaryGap": false,
"data": ["1号", "2号", "3号"]
},
"yAxis": {
"type": "value"
},
"series": [
{
"name": "用户数",
"type": "line",
"stack": "总量",
"data": [10, 20, 30]
}
]
}
【【【【【
根据提供的数据,我们可以得出以下结论:
从1号到3号,网站用户数呈持续增长趋势,从10增加到30,增长了200%。
每天的用户增长数是:1号到2号增长了10个用户,2号到3号增长了10个用户。
如果这个增长趋势持续,可以预期未来网站用户数将继续上升。需要进一步关注用户增长的速度以及可能影响增长的因素,以制定相应的市场策略。
【3】指定示例问答
修改模型信息,点击【高级设置】=》【导入示例】=【更新AI助手】
【用户】:将【1】中的提问填充进去
【助手】:将【2】中的回答填充进去
然后保存,继续问同一个问题
【【【【【
{
"tooltip": {
"trigger": 'axis'
},
"legend": {
"data": ['用户数']
},
"xAxis": {
"type": 'category',
"data": ['1号', '2号', '3号']
},
"yAxis": {
"type": 'value'
},
"series": [
{
"name": '用户数',
"type": 'line',
"data": [10, 20, 30]
}
]
}
【【【【【
根据提供的数据,我们可以得出以下结论:
网站用户数从1号到3号呈现线性增长,每天增长10个用户。
在这三天内,用户数从10增长到30,增长了200%。
平均每日增长率约为66.67%。
如果这种增长趋势持续下去,可以预见网站用户数将稳步上升。
2.三种调用AI的方式
如果在程序中想传递参数给AI,并且得到AI返回值,则可通过调用接口实现
(1)调用官方接口
比如OpenAI或者其他Al原始大模型官网的接口官方文档
优点:不经封装,最灵活,最原始
缺点:要钱、要魔法
本质上OpenAI就是提供了HTTP接口,可以用任何语言去调用
调用步骤参考
【1】在请求头指定OPEN_API_KEY => Authorization:Bearer OPENAI_API_KEY,否则提示无权限调用
【2】找到要使用的接口(例如AI对话接口)
【3】按照接口文档示例,构造HTTP请求(借助hutool工具类、httpclient工具类)
public class OpenAiApi {
public static void main(String[] args) {
/**
* AI 对话(需要自己创建请求响应对象)
* @param request
* @param openAiApiKey
* @return
*/
public CreateChatCompletionResponse createChatCompletion(CreateChatCompletionRequest request, String openAiApiKey) {
if (StringUtils.isBlank(openAiApiKey)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 openAiApiKey");
}
String url = "https://api.openai.com/v1/chat/completions";
String json = JSONUtil.toJsonStr(request);
String result = HttpRequest.post(url)
.header("Authorization", "Bearer " + openAiApiKey)
.body(json)
.execute()
.body();
return JSONUtil.toBean(result, CreateChatCompletionResponse.class);
}
}
}
然后获取到响应数据
(2)使用云服务商提供的封装接口
例如:Azure云(微软官方)
- 优点:本地可用
- 缺点:要钱,比直接调用原始接口更贵
(3)鱼AI
- 优点:一定额度免费,有很多现成的模型(prompt系统预设)
- 缺点:不完全灵活,可自定义模型
鼠标移动到头像=》开放平台=》得到开发者秘钥,下载SDK(SDK项目文档)
SDK依赖配置
<dependency>
<groupId>com.yucongming</groupId>
<artifactId>yucongming-java-sdk</artifactId>
<version>0.0.3</version>
</dependency>
构建YuCongMingClient对象(配置application.yml)
# 鱼聪明 AI 配置(https://yucongming.com/)
yuapi:
client:
access-key: 替换为你自己的 access-key
secret-key: 替换为你自己的 secret-key
在manager包下创建AiManager.java(对接第三方接口)
@Service
public class AiManager {
@Resource
private YuCongMingClient yuCongMingClient;
/**
* AI 对话
* @param modelId
* @param message
* @return
*/
public String doChat(long modelId, String message) {
DevChatRequest devChatRequest = new DevChatRequest();
devChatRequest.setModelId(modelId);
devChatRequest.setMessage(message);
BaseResponse<DevChatResponse> response = yuCongMingClient.doChat(devChatRequest);
if (response == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "AI 响应错误");
}
return response.getData().getContent();
}
}
分享助手,然后选择复制链接(我发现了一个 AI 对话助手,点击链接进行聊天:https://www.yucongming.com/model/1780947646559985666?inviteUser=1765656249917460482),从连接中获取到模型id(1780947646559985666)
方法名: doChat
请求参数:
modelId:使用的会话模型id
message :要发送的消息,不超过1024 字
响应结果:
code :响应状态码
data :
content:对话结果内容
message :响应信息
测试类构建
@SpringBootTest
class AiManagerTest {
@Resource
private AiManager aiManager;
@Test
void doChat() {
String answer = aiManager.doChat(1780947646559985666L, "分析需求:\n" +
"分析网站用户的增长情况\n" +
"原始数据:\n" +
"日期,用户数\n" +
"1号,10\n" +
"2号,20\n" +
"3号,30");
System.out.println(answer);
}
}
3.业务逻辑编写
创建model/vo/BiResponse(Bi返回结果定义)
@Data
public class BiResponse {
private String genChart;
private String genResult;
private Long chartId;
}
优化ChartController中的实现、CommonConstant(定义ModelId)
public interface CommonConstant {
/**
* AI modelId
*/
long BI_MODEL_ID = 1780947646559985666L;
}
@PostMapping("/upload")
public BaseResponse<BiResponse> genChartByAi(@RequestPart("file") MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
String name = genChartByAiRequest.getName();
String goal = genChartByAiRequest.getGoal();
String chartType = genChartByAiRequest.getChartType();
// 校验
ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空");
ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100, ErrorCode.PARAMS_ERROR, "名称过长");
User loginUser = userService.getLoginUser(request);
long biModelId = CommonConstant.BI_MODEL_ID;
// 构造用户输入
StringBuilder userInput = new StringBuilder();
userInput.append("分析需求:").append("\n");
// 拼接分析目标
String userGoal = goal;
if (StringUtils.isNotBlank(chartType)) {
userGoal += ",请使用" + chartType;
}
userInput.append(userGoal).append("\n");
userInput.append("原始数据:").append("\n");
// 压缩后的数据
String csvData = ExcelUtils.excelToCsv(multipartFile);
userInput.append(csvData).append("\n");
String result = aiManager.doChat(biModelId, userInput.toString());
// 此处分隔符以设定为参考
String[] splits = result.split("【【【【【"); // 【【【【【
if (splits.length < 3) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "AI 生成错误");
}
String genChart = splits[1].trim();
String genResult = splits[2].trim();
// 插入到数据库
Chart chart = new Chart();
chart.setName(name);
chart.setGoal(goal);
chart.setChartData(csvData);
chart.setChartType(chartType);
chart.setGenChart(genChart);
chart.setGenResult(genResult);
chart.setUserId(loginUser.getId());
boolean saveResult = chartService.save(chart);
ThrowUtils.throwIf(!saveResult, ErrorCode.SYSTEM_ERROR, "图表保存失败");
BiResponse biResponse = new BiResponse();
biResponse.setGenChart(genChart);
biResponse.setGenResult(genResult);
biResponse.setChartId(chart.getId());
return ResultUtils.success(biResponse);
}
将从控制台获取到的图表信息,通过Echarts在线调试,确认是否可以生成图表
==> Parameters: 详细分析网站用户增长情况(String), 用户增长表(String), 日期,用户数
1号,10
2号,20
3号,30
4号,90
5号,0
6号,10
7号,20
(String), {
"title": {
"text": "网站用户增长情况"
},
"tooltip": {
"trigger": "axis"
},
"legend": {
"data": ["用户数"]
},
"grid": {
"left": "3%",
"right": "4%",
"bottom": "3%",
"containLabel": true
},
"toolbox": {
"feature": {
"saveAsImage": {}
}
},
"xAxis": {
"type": "category",
"boundaryGap": false,
"data": ["1号", "2号", "3号", "4号", "5号", "6号", "7号"]
},
"yAxis": {
"type": "value"
},
"series": [
{
"name": "用户数",
"type": "line",
"data": [10, 20, 30, 90, 0, 10, 20],
"markPoint": {
"data": [
{ "type": "max", "name": "最大值" },
{ "type": "min", "name": "最小值" }
]
},
"markLine": {
"data": [
{ "type": "average", "name": "平均值" }
]
}
}
]
}(String), 根据提供的数据,我们可以进行以下详细分析:
- 用户增长趋势:从1号到3号,用户数呈现稳步增长,从10增加到30。然而,4号用户数大幅增长至90,可能是由于某些推广活动或特殊情况。5号用户数突然下降至0,可能是数据错误或网站维护等原因。从6号开始,用户数又恢复到10,并在7号增长到20。
- 用户增长波动:从1号到7号,用户数的平均值为30(计算平均时排除5号的0,因为可能是异常数据)。最大值为90(出现在4号),最小值为0(出现在5号)。
- 用户增长幅度:从1号到4号,用户增长了800%。但从4号到5号,用户数下降了100%,这是一个显著的波动。从5号到7号,用户数增长了100%。
- 异常情况:5号的数据明显异常,需要进一步调查原因,可能是数据录入错误或网站问题。
- 结论:整体来看,除了5号的异常数据外,网站用户数在观察期间呈现增长趋势,但增长幅度存在较大波动,需要进一步分析影响增长的具体因素。特别是4号的显著增长和5号的显著下降,需要重点关注和调查。(String), 1780252549874569217(Long)
进一步检查数据库数据是否插入成功:
至此,完成BI智能接口的后端接口开发
4.后端接口开发总结
后端接口开发流程
【1】梳理AI接口接入思路(定义交互格式)
【2】构造用户请求(用户消息、CSV数据、图表类型)
【3】调用AI接口,获取并处理AI响应结果(获取对应图表信息)
【4】将响应数据(图表信息)保存到数据库中(前端可渲染数据信息)
前端开发
1.用户表单开发
组件页面构建
创建AddChart组件(pages/AddChart/index.tsx:可以从User下copy一份),配置routes.ts路由
routes.ts路由配置
// { path: '/', redirect: '/welcome' },
{ path: '/', redirect: '/add_chart' },
{ name: '添加图表', icon: 'table', path: '/add_chart', component: './AddChart' },
AddChart.tsx组件
import { Footer } from '@/components';
import { listChartByPageUsingPost } from '@/services/noob-bi/chartController';
import { useModel } from '@umijs/max';
import React, { useEffect,useState } from 'react';
const AddChart: React.FC = () => {
const [type, setType] = useState<string>('account');
const { setInitialState } = useModel('@@initialState');
useEffect(()=>{
listChartByPageUsingPost({}).then(res=>{
console.error('res',res)
})
});
return (
// 页面信息定义(add-chart)
<div className = "add-chart">
hello my chart
</div>
);
};
export default AddChart;
生成效果
表单开发
选择参考表单代码
import React from 'react';
import { InboxOutlined, UploadOutlined } from '@ant-design/icons';
import {
Button,
Checkbox,
Col,
ColorPicker,
Form,
InputNumber,
Radio,
Rate,
Row,
Select,
Slider,
Space,
Switch,
Upload,
} from 'antd';
const { Option } = Select;
const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 },
};
const normFile = (e: any) => {
console.log('Upload event:', e);
if (Array.isArray(e)) {
return e;
}
return e?.fileList;
};
const onFinish = (values: any) => {
console.log('Received values of form: ', values);
};
const App: React.FC = () => (
<Form
name="validate_other"
{...formItemLayout}
onFinish={onFinish}
initialValues={{
'input-number': 3,
'checkbox-group': ['A', 'B'],
rate: 3.5,
'color-picker': null,
}}
style={{ maxWidth: 600 }}
>
<Form.Item label="Plain Text">
<span className="ant-form-text">China</span>
</Form.Item>
<Form.Item
name="select"
label="Select"
hasFeedback
rules={[{ required: true, message: 'Please select your country!' }]}
>
<Select placeholder="Please select a country">
<Option value="china">China</Option>
<Option value="usa">U.S.A</Option>
</Select>
</Form.Item>
<Form.Item
name="select-multiple"
label="Select[multiple]"
rules={[{ required: true, message: 'Please select your favourite colors!', type: 'array' }]}
>
<Select mode="multiple" placeholder="Please select favourite colors">
<Option value="red">Red</Option>
<Option value="green">Green</Option>
<Option value="blue">Blue</Option>
</Select>
</Form.Item>
<Form.Item label="InputNumber">
<Form.Item name="input-number" noStyle>
<InputNumber min={1} max={10} />
</Form.Item>
<span className="ant-form-text" style={{ marginLeft: 8 }}>
machines
</span>
</Form.Item>
<Form.Item name="switch" label="Switch" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="slider" label="Slider">
<Slider
marks={{
0: 'A',
20: 'B',
40: 'C',
60: 'D',
80: 'E',
100: 'F',
}}
/>
</Form.Item>
<Form.Item name="radio-group" label="Radio.Group">
<Radio.Group>
<Radio value="a">item 1</Radio>
<Radio value="b">item 2</Radio>
<Radio value="c">item 3</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="radio-button"
label="Radio.Button"
rules={[{ required: true, message: 'Please pick an item!' }]}
>
<Radio.Group>
<Radio.Button value="a">item 1</Radio.Button>
<Radio.Button value="b">item 2</Radio.Button>
<Radio.Button value="c">item 3</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item name="checkbox-group" label="Checkbox.Group">
<Checkbox.Group>
<Row>
<Col span={8}>
<Checkbox value="A" style={{ lineHeight: '32px' }}>
A
</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="B" style={{ lineHeight: '32px' }} disabled>
B
</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="C" style={{ lineHeight: '32px' }}>
C
</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="D" style={{ lineHeight: '32px' }}>
D
</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="E" style={{ lineHeight: '32px' }}>
E
</Checkbox>
</Col>
<Col span={8}>
<Checkbox value="F" style={{ lineHeight: '32px' }}>
F
</Checkbox>
</Col>
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item name="rate" label="Rate">
<Rate />
</Form.Item>
<Form.Item
name="upload"
label="Upload"
valuePropName="fileList"
getValueFromEvent={normFile}
extra="longgggggggggggggggggggggggggggggggggg"
>
<Upload name="logo" action="/upload.do" listType="picture">
<Button icon={<UploadOutlined />}>Click to upload</Button>
</Upload>
</Form.Item>
<Form.Item label="Dragger">
<Form.Item name="dragger" valuePropName="fileList" getValueFromEvent={normFile} noStyle>
<Upload.Dragger name="files" action="/upload.do">
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">Click or drag file to this area to upload</p>
<p className="ant-upload-hint">Support for a single or bulk upload.</p>
</Upload.Dragger>
</Form.Item>
</Form.Item>
<Form.Item
name="color-picker"
label="ColorPicker"
rules={[{ required: true, message: 'color is required!' }]}
>
<ColorPicker />
</Form.Item>
<Form.Item wrapperCol={{ span: 12, offset: 6 }}>
<Space>
<Button type="primary" htmlType="submit">
Submit
</Button>
<Button htmlType="reset">reset</Button>
</Space>
</Form.Item>
</Form>
);
export default App;
AddChart雏形
填充Form表单(清理无用代码)、引入onFinish方法、导入所需依赖,简化后的代码参考如下
import { Footer } from '@/components';
import { listChartByPageUsingPost } from '@/services/noob-bi/chartController';
import { useModel } from '@umijs/max';
import React, { useEffect,useState } from 'react';
import { InboxOutlined, UploadOutlined } from '@ant-design/icons';
import {
Button,
Form,
Select,
Space,
Upload,
} from 'antd';
import TextArea from 'antd/es/input/TextArea';
const AddChart: React.FC = () => {
const [type, setType] = useState<string>('account');
const { setInitialState } = useModel('@@initialState');
useEffect(()=>{
listChartByPageUsingPost({}).then(res=>{
console.error('res',res)
})
});
const onFinish = (values: any) => {
console.log('Received values of form: ', values);
};
return (
// 页面信息定义(add-chart)
<div className = "add-chart">
<Form
// 设定表单名称
name="addChart"
onFinish={onFinish}
// 初始化数据为空
initialValues={{ }}
style={{ maxWidth: 600 }}
>
<Form.Item name="rate" label="Rate">
<TextArea />
</Form.Item>
<Form.Item
name="select"
label="Select"
hasFeedback
rules={[{ required: true, message: 'Please select your country!' }]}
>
<Select placeholder="Please select a country">
<Option value="china">China</Option>
<Option value="usa">U.S.A</Option>
</Select>
</Form.Item>
<Form.Item
name="upload"
label="Upload"
valuePropName="fileList"
extra="longgggggggggggggggggggggggggggggggggg"
>
<Upload name="logo" action="/upload.do" listType="picture">
<Button icon={<UploadOutlined />}>Click to upload</Button>
</Upload>
</Form.Item>
<Form.Item wrapperCol={{ span: 12, offset: 6 }}>
<Space>
<Button type="primary" htmlType="submit">
Submit
</Button>
<Button htmlType="reset">reset</Button>
</Space>
</Form.Item>
</Form>
</div>
);
};
export default AddChart;
AddChart细化
import { Footer } from '@/components';
import { listChartByPageUsingPost } from '@/services/noob-bi/chartController';
import { useModel } from '@umijs/max';
import React, { useEffect,useState } from 'react';
import { InboxOutlined, UploadOutlined } from '@ant-design/icons';
import {
Button,
Form,
Input,
Select,
Space,
Upload,
} from 'antd';
import TextArea from 'antd/es/input/TextArea';
const AddChart: React.FC = () => {
const [type, setType] = useState<string>('account');
const { setInitialState } = useModel('@@initialState');
useEffect(()=>{
listChartByPageUsingPost({}).then(res=>{
console.error('res',res)
})
});
const onFinish = (values: any) => {
console.log('Received values of form: ', values);
};
return (
// 页面信息定义(add-chart)
<div className = "add-chart">
<Form
// 设定表单名称
name="addChart"
onFinish={onFinish}
// 初始化数据为空
initialValues={{ }}
style={{ maxWidth: 600 }}
>
{/* 前端表单的name属性对应后端接口请求参数的字段,name对应goal,label为左侧提示文本,rules=....是必填项提示 */}
<Form.Item name="goal" label="分析目标" rules={[{required:true,message:"请输入分析目标"}]}>
{/* placeholder文本框提示语 */}
<TextArea placeholder="请输入分析需求,例如:分析网站用户的增长情况"/>
</Form.Item>
{/* 图表名称 */}
<Form.Item name="name" label="图表名称">
{/* placeholder文本框提示语 */}
<Input placeholder="请输入分析需求,例如:分析网站用户的增长情况"/>
</Form.Item>
{/* 图表类型非必填,不做校验 */}
<Form.Item
name="selchartTypeect"
label="图表类型"
>
<Select placeholder="请选择图表类型"
options={[
{value:'折线图',label:'折线图'},
{value:'柱状图',label:'柱状图'},
{value:'堆叠图',label:'堆叠图'},
{value:'饼图',label:'饼图'},
{value:'雷达图',label:'雷达图'},
]}
/>
</Form.Item>
{/* 文件上传 */}
<Form.Item
name="file"
label="原始数据"
>
{/* action:文件上传之后 调用后台接口 action="/upload.do" */}
<Upload name="file">
<Button icon={<UploadOutlined />}>上传CSV文件</Button>
</Upload>
</Form.Item>
<Form.Item wrapperCol={{ span: 12, offset: 6 }}>
<Space>
<Button type="primary" htmlType="submit">
提交
</Button>
<Button htmlType="reset">重置</Button>
</Space>
</Form.Item>
</Form>
</div>
);
};
export default AddChart;
2.后端对接
利用openapi自动生成前端调用代码,启动npm run openapi ,生成接口调用代码
简化前端模板:将src/services目录下的ant-design-pro删除(测试接口),并且去除相关引用(后续调整为后端对接相关接口)
删除pages/TableList文件夹,并在routes.ts中去除相关路由
完善onFinish方法,完成后端接口调用
接口调用通过断点调试,或者F12查看响应结果(接口调用AI响应可能有点慢,耐心蹲)
3.图表渲染
图表渲染选择Echart库(在API接口平台已经了解过使用)
【1】引入echarts组件库
npm install echarts-for-react
【2】参考官方示例,编写示例代码看能否接入Echarts
【3】联调测试(完整代码测试)
import { genChartByAiUsingPost, listChartByPageUsingPost } from '@/services/noob-bi/chartController';
import { useModel } from '@umijs/max';
import React, { useEffect, useState } from 'react';
import { UploadOutlined } from '@ant-design/icons';
import {
Button,
Form,
Input,
Select,
Space,
Upload,
message,
Card,
Col,
Row,
Spin,
Divider
} from 'antd';
import TextArea from 'antd/es/input/TextArea';
import EChartsReact from 'echarts-for-react';
const AddChart: React.FC = () => {
// 定义状态,接收后端返回值,实时展示在页面上
const [chart,setChart] = useState<API.BiResponse>();
const [option,setOption] = useState<any>();
// 提交中的状态,默认未提交
const [submitting,setSubmitting] = useState<boolean>(false);
/**
* 提交
* @param values
*/
const onFinish = async (values: any) => {
console.log('Received values of form: ', values);
// 如果已经提交中的状态(加载),则直接返回避免重复提交
if(submitting){
return;
}
// 开始提交,将submitting设置为true
setSubmitting(true);
// 如果提交了,则将图表数据和图表代码清空(避免和之前提交的图表堆叠);如果option清空了则组件会触发重新渲染,不会保留之前的历史记录
setChart(undefined);
setOption(undefined);
// 对接后端,上传数据
const params = {
...values,
file: undefined,
};
try {
// 获取到上传的原始数据并传入后端接口
const res = await genChartByAiUsingPost(params, {}, values.file.file.originFileObj);
// 一般情况下没有返回值为分析失败,有则认为成功
if (!res?.data) {
message.error('分析失败');
} else {
message.success('分析成功');
// 解析成对象,为空则设置为空字符串
const chartOption = JSON.parse(res.data.genChart??'');
// 如果为空,则抛出异常,提示图表代码解析错误
if(!chartOption){
throw new Error('图表代码解析错误');
}else{
// 解析成功,则将响应结果设置到图表中进行渲染
setChart(res.data);
setOption(chartOption);
}
}
} catch (e: any) {
// 异常情况下,提示分析失败和具体的原因说明
message.error('分析失败,' + e.message);
}
// 提交结束,将setSubmitting设置为false
setSubmitting(false);
};
return (
// 页面展示美化
// 页面信息定义(add-chart)
<div className = "add-chart">
<Row gutter={24}>
<Col span={12}>
<Card title="智能分析">
<Form
// 设定表单名称
name="addChart"
onFinish={onFinish}
// 初始化数据为空
initialValues={{ }}
style={{ maxWidth: 600 }}
>
{/* 前端表单的name属性对应后端接口请求参数的字段,name对应goal,label为左侧提示文本,rules=....是必填项提示 */}
<Form.Item name="goal" label="分析目标" rules={[{required:true,message:"请输入分析目标"}]}>
{/* placeholder文本框提示语 */}
<TextArea placeholder="请输入分析需求,例如:分析网站用户的增长情况"/>
</Form.Item>
{/* 图表名称 */}
<Form.Item name="name" label="图表名称">
{/* placeholder文本框提示语 */}
<Input placeholder="请输入分析需求,例如:分析网站用户的增长情况"/>
</Form.Item>
{/* 图表类型非必填,不做校验 */}
<Form.Item
name="selchartTypeect"
label="图表类型"
>
<Select placeholder="请选择图表类型"
options={[
{value:'折线图',label:'折线图'},
{value:'柱状图',label:'柱状图'},
{value:'堆叠图',label:'堆叠图'},
{value:'饼图',label:'饼图'},
{value:'雷达图',label:'雷达图'},
]}
/>
</Form.Item>
{/* 文件上传 */}
<Form.Item
name="file"
label="原始数据"
>
{/* action:文件上传之后 调用后台接口 action="/upload.do" */}
<Upload name="file">
<Button icon={<UploadOutlined />}>上传CSV文件</Button>
</Upload>
</Form.Item>
<Form.Item wrapperCol={{ span: 12, offset: 6 }}>
<Space>
<Button type="primary" htmlType="submit">
提交
</Button>
<Button htmlType="reset">重置</Button>
</Space>
</Form.Item>
</Form>
</Card>
</Col>
<Col span={12}>
<Card title="分析结论">
{chart?.genResult ?? <div>请先在左侧进行提交</div>}
<Spin spinning={submitting}/>
</Card>
<Divider />
<Card title="可视化图表">
{
option ? <EChartsReact option={option} /> : <div>请先在左侧进行提交</div>
}
<Spin spinning={submitting}/>
</Card>
</Col>
</Row>
</div>
);
};
export default AddChart;
待优化
【1】网站安全性考虑
如果用户上传一个超级大的文件怎么办
如果用户疯狂点击提交怎么办
如果AI生成太慢,但是用户访问量比较大,给系统造成压力,如何兼顾用户体验和系统的可用性