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_requireddef 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) }