Last updated on

交易管理系统“收益统计”界面折线图显示问题复盘

查看api对接

由于一开始的按钮绑定的onToggleChart之切换了显示视图,没有绑定数据请求相关的动作,因此只能先通过前后端的api层去查看该部分可能用到的api是什么,通过命名、注释判断,找到后端的controller层所需要接受的参数,以及Service里面处理数据业务的内容,方便后续处理

查看前端所需数据的格式

pinia

此项目的代码,连接api层、初步处理后端数据的内容都是在store中,找到对应的store的文件,寻找getData相关的函数

async getChartData(data = null) {
if (data) this.tradeChartRequest = data;
this.refreshTime = CommonHelper.timeFormatter();
this.status = Constants.CODE.SUCCESS;
try {
const resp = await API.stat.tradeChart(this.tradeChartRequest);
this.status = resp.code;
//数据处理部分是我们数据链路需要关注的
if (resp.code === Constants.CODE.SUCCESS) {
this.message = '';
const resData = resp.data;
//对每一项数据进行的处理
resData.forEach(item => {
//后端返回的每一个对象内,都有一个data属性,且是一个 JSON 字符串
item.data = JSON.parse(item.data);
});
this.profitData = resData;
} else {
this.message = resp.message;
OakNotification.error('获取交易账户收益信息失败', resp.message);
}
} catch (error) {
handleOpFail(this, error, '获取交易账户收益信息失败');
}
},

此处主要分析对每一项的处理部分

Chart

关乎渲染的图片,我们去详细查看前端echart对应的组件TradeProfitChart.vue,特别是数据渲染的函数

const dataFn = (data, type) => {
if(!chart){
console.log('chart is null!');
return [];
}
if(!data || !data.length){
return []
}
const names = [];
const dates = [];
//每条曲线要有 nameCn
option.legend.data = data.map((item) => {
return item.nameCn;
});
//每条曲线还要有 date,而且也是 JSON 字符串数组
if(data && data.length){
option.xAxis.data = JSON.parse(data[0].date);
}
else{
option.xAxis.data = [];
}
//item.data 最终必须是数组,里面是净值点
option.series = data.map((item) => {
return {
name: item.nameCn,
type: 'line',
data: item.data,
}
});
chart.setOption(option, true);
};

因此前端要获取到的数据格式是如下:

[
{
"nameCn": "丹枫1.4.0",
"date": "[\"2025-11-07\",\"2025-11-14\"]",
"data": "[1.22,1.23]"
},
{
"nameCn": "丹枫1.4.1",
"date": "[\"2025-11-07\",\"2025-11-14\"]",
"data": "[1.42,1.43]"
}
]

查看后端需要前端发送的参数

找到controller,看到

@statistics_bp.route('/StockAccChart', methods=['POST'])
@auth.login_required
def stock_acc_chart():
"""股票账户收益折线图接口"""
req = request.json
name_cn = req.get('nameCn')
# nameCn 不是单个字符串,而是一个 JSON 字符串化后的数组
name_arr = json.loads(name_cn) if name_cn else []
data = StockService.stock_acc_chart(name_arr, req.get('startTime'), req.get('endTime'), req.get('frequency'))
return Result.success_data(data=data, msg='查询成功')

所以 API 层要求前端发的其实是:

{
"nameCn": "[\"丹枫1.4.0\",\"丹枫1.4.1\"]",
"startTime": "2025-11-01",
"endTime": "2026-03-20",
"frequency": "week"
}

查看后端详细业务

@staticmethod
def stock_acc_chart(name_arr, start_time, end_time, frequency):
if not name_arr:
return []
# 取完整日期轴
rows = []
sql = f'select * from comm_calendar where calendarday between "{start_time}" and "{end_time}" order by calendarday'
date = []
dateList = db.session.execute(text(sql))
for item in dateList:
date.append(str(item.calendarday))
# frequency做过滤规则
if frequency == 'day':
calendar_filter = ""
elif frequency == 'week':
calendar_filter = "and a.tradingday = b.calendarday and b.ifweekend = 1"
elif frequency == 'month':
calendar_filter = "and a.tradingday = b.calendarday and b.ifmonthend = 1"
else:
calendar_filter = ""
# 是否是多账户查询,后续序列化要处理
is_multi = len(name_arr) > 1
# 数据库查询
for item in name_arr:
if frequency == 'day':
sql = f'select tradingday,net_value from securities_info_daily where tradingday between "{start_time}" and "{end_time}" and securities_account_name_cn = "{item}" order by tradingday ;'
else:
sql = f'select a.tradingday as tradingday,a.net_value as net_value from securities_info_daily as a,comm_calendar as b where tradingday between "{start_time}" and "{end_time}" and securities_account_name_cn = "{item}" {calendar_filter} order by tradingday ;'
lists = db.session.execute(text(sql))
rows.append(ProductInfoDaily.statisticsProfitSerialized(item, lists, 1 if is_multi else 0, date)) # Using ProductInfoDaily util for securities too, as per original code
rows = ProductInfoDaily.ReformatDate(rows)
db.session.remove()
return rows

实际上,Service做的就是,从securities_info_daily表中根据频率、日期(这二者依据的是comm_calendar表),找出需要的数据,后续就是序列化成前端需要的格式(上一节分析的)

但是我发现statisticsProfitSerialized函数不存在,本来想复用序列化函数,但是由于前端需要的数据格式不一样,且前端的echarts渲染已经可用,需要调整的话不如新增一个格式化函数方便,所以我先做了后端的格式化函数

@staticmethod
def statisticsChartSerialized(name_cn, lists, date):
data_map = {}
for item in lists:
data_map[str(item.tradingday)] = DecimalEncoder(item.net_value)
return {
'nameCn': name_cn,
'date': json.dumps(date),
'data': json.dumps([data_map.get(day) for day in date])
}

接着修改了一下Service,使其功能不变,可读性更强,也不需要在取数据时再去查询日历表了,现在的逻辑是,按照日期要求去取出要的日期点,然后去查询净值数据时直接复用已经有的数据点,然后调用函数格式化返回前端

@staticmethod
def stock_acc_chart(name_arr, start_time, end_time, frequency):
if not name_arr:
return []
rows = []
date = []
if frequency == 'week':
sql = f'select calendarday from comm_calendar where calendarday between "{start_time}" and "{end_time}" and ifweekend = 1 order by calendarday'
elif frequency == 'month':
sql = f'select calendarday from comm_calendar where calendarday between "{start_time}" and "{end_time}" and ifmonthend = 1 order by calendarday'
elif frequency == 'year':
sql = f'select calendarday from comm_calendar where calendarday between "{start_time}" and "{end_time}" and ifyearend = 1 order by calendarday'
else:
sql = f'select calendarday from comm_calendar where calendarday between "{start_time}" and "{end_time}" and iftrading = 1 order by calendarday'
dateList = db.session.execute(text(sql))
for item in dateList:
date.append(str(item.calendarday))
if not date:
db.session.remove()
return rows
date_sql = '","'.join(date)
for item in name_arr:
sql = f'select tradingday,net_value from securities_info_daily where tradingday in ("{date_sql}") and securities_account_name_cn = "{item}" order by tradingday ;'
lists = db.session.execute(text(sql))
rows.append(SecuritiesInfoDaily.statisticsChartSerialized(item, lists, date))
db.session.remove()
return rows

后续优化

由于数据库访问两次会增加性能开销,因此合并了一下

@staticmethod
def stock_acc_chart(name_arr, start_time, end_time, frequency):
if not name_arr:
return []
rows = []
calendar_filter = f'and b.if{frequency}end = 1' if frequency in ('week', 'month', 'year') else 'and b.iftrading = 1'
for item in name_arr:
sql = f'''
select b.calendarday as tradingday, a.net_value as net_value
from comm_calendar as b
left join securities_info_daily as a
on a.tradingday = b.calendarday
and a.securities_account_name_cn = "{item}"
where b.calendarday between "{start_time}" and "{end_time}"
{calendar_filter}
order by b.calendarday;
'''
lists = db.session.execute(text(sql))
rows.append(SecuritiesInfoDaily.statisticsChartSerialized(item, lists))
db.session.remove()
return rows
@staticmethod
def statistics_chart_serialized(name_cn, lists):
date = []
data = []
for item in lists:
date.append(str(item.tradingday))
data.append(DecimalEncoder(item.net_value))
return {
'nameCn': name_cn,
'date': json.dumps(date),
'data': json.dumps(data)
}