Files
vf_react/src/services/financialService.js
2025-10-20 21:25:33 +08:00

413 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { getApiBase } from '../utils/apiConfig';
// src/services/financialService.js
/**
* 完整的财务数据服务层
* 对应Flask后端的所有财务API接口
*/
import { logger } from '../utils/logger';
const isProduction = process.env.NODE_ENV === 'production';
const API_BASE_URL = getApiBase();
const apiRequest = async (url, options = {}) => {
try {
logger.debug('financialService', 'API请求', {
url: `${API_BASE_URL}${url}`,
method: options.method || 'GET'
});
const response = await fetch(`${API_BASE_URL}${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
credentials: 'include', // 包含 cookies以便后端识别登录状态
});
if (!response.ok) {
const errorText = await response.text();
logger.error('financialService', 'apiRequest', new Error(`HTTP ${response.status}`), {
url,
status: response.status,
errorText: errorText.substring(0, 200)
});
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
logger.debug('financialService', 'API响应', {
url,
success: data.success,
hasData: !!data.data
});
return data;
} catch (error) {
logger.error('financialService', 'apiRequest', error, { url });
throw error;
}
};
export const financialService = {
/**
* 获取股票基本信息和最新财务摘要
* @param {string} seccode - 股票代码
*/
getStockInfo: async (seccode) => {
return await apiRequest(`/api/financial/stock-info/${seccode}`);
},
/**
* 获取完整的资产负债表数据
* @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量
*/
getBalanceSheet: async (seccode, limit = 12) => {
return await apiRequest(`/api/financial/balance-sheet/${seccode}?limit=${limit}`);
},
/**
* 获取完整的利润表数据
* @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量
*/
getIncomeStatement: async (seccode, limit = 12) => {
return await apiRequest(`/api/financial/income-statement/${seccode}?limit=${limit}`);
},
/**
* 获取完整的现金流量表数据
* @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量
*/
getCashflow: async (seccode, limit = 12) => {
return await apiRequest(`/api/financial/cashflow/${seccode}?limit=${limit}`);
},
/**
* 获取完整的财务指标数据
* @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量
*/
getFinancialMetrics: async (seccode, limit = 12) => {
return await apiRequest(`/api/financial/financial-metrics/${seccode}?limit=${limit}`);
},
/**
* 获取主营业务构成数据
* @param {string} seccode - 股票代码
* @param {number} periods - 获取的报告期数量
*/
getMainBusiness: async (seccode, periods = 4) => {
return await apiRequest(`/api/financial/main-business/${seccode}?periods=${periods}`);
},
/**
* 获取业绩预告和预披露时间
* @param {string} seccode - 股票代码
*/
getForecast: async (seccode) => {
return await apiRequest(`/api/financial/forecast/${seccode}`);
},
/**
* 获取行业排名数据
* @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量
*/
getIndustryRank: async (seccode, limit = 4) => {
return await apiRequest(`/api/financial/industry-rank/${seccode}?limit=${limit}`);
},
/**
* 获取不同报告期的对比数据
* @param {string} seccode - 股票代码
* @param {number} periods - 对比的报告期数量
*/
getPeriodComparison: async (seccode, periods = 8) => {
return await apiRequest(`/api/financial/comparison/${seccode}?periods=${periods}`);
},
};
// 数据格式化工具函数
export const formatUtils = {
/**
* 格式化大数字(亿/万)
* @param {number} num - 数字
* @param {number} decimal - 小数位数
*/
formatLargeNumber: (num, decimal = 2) => {
if (num === null || num === undefined) return '-';
const absNum = Math.abs(num);
let result = '';
if (absNum >= 100000000) {
result = (num / 100000000).toFixed(decimal) + '亿';
} else if (absNum >= 10000) {
result = (num / 10000).toFixed(decimal) + '万';
} else if (absNum >= 1) {
result = num.toFixed(decimal);
} else {
result = num.toFixed(4); // 小于1的数字显示更多小数位
}
return result;
},
/**
* 格式化百分比
* @param {number} num - 数字
* @param {number} decimal - 小数位数
*/
formatPercent: (num, decimal = 2) => {
if (num === null || num === undefined) return '-';
return num.toFixed(decimal) + '%';
},
/**
* 格式化日期
* @param {string} dateStr - 日期字符串
*/
formatDate: (dateStr) => {
if (!dateStr) return '-';
const date = new Date(dateStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
},
/**
* 获取报告期类型
* @param {string} dateStr - 日期字符串
*/
getReportType: (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const month = date.getMonth() + 1;
const year = date.getFullYear();
switch(month) {
case 3:
return `${year}Q1`;
case 6:
return `${year}中报`;
case 9:
return `${year}Q3`;
case 12:
return `${year}年报`;
default:
return dateStr;
}
},
/**
* 格式化增长率颜色用于Chakra UI
* @param {number} value - 增长率数值
*/
getGrowthColor: (value) => {
if (value === null || value === undefined) return 'gray.500';
if (value > 0) return 'green.500';
if (value < 0) return 'red.500';
return 'gray.500';
},
/**
* 获取指标变化趋势图标
* @param {number} current - 当前值
* @param {number} previous - 之前的值
*/
getTrendIcon: (current, previous) => {
if (!current || !previous) return 'stable';
if (current > previous) return 'up';
if (current < previous) return 'down';
return 'stable';
},
/**
* 计算同比增长率
* @param {number} current - 当前值
* @param {number} yearAgo - 去年同期值
*/
calculateYoY: (current, yearAgo) => {
if (!current || !yearAgo) return null;
return ((current - yearAgo) / Math.abs(yearAgo)) * 100;
},
/**
* 计算环比增长率
* @param {number} current - 当前值
* @param {number} previous - 上期值
*/
calculateQoQ: (current, previous) => {
if (!current || !previous) return null;
return ((current - previous) / Math.abs(previous)) * 100;
},
/**
* 获取财务健康度评分
* @param {object} metrics - 财务指标对象
*/
getFinancialHealthScore: (metrics) => {
let score = 0;
let factors = 0;
// ROE评分
if (metrics.roe !== null && metrics.roe !== undefined) {
factors++;
if (metrics.roe > 20) score += 5;
else if (metrics.roe > 15) score += 4;
else if (metrics.roe > 10) score += 3;
else if (metrics.roe > 5) score += 2;
else if (metrics.roe > 0) score += 1;
}
// 流动比率评分
if (metrics.current_ratio !== null && metrics.current_ratio !== undefined) {
factors++;
if (metrics.current_ratio > 2) score += 5;
else if (metrics.current_ratio > 1.5) score += 4;
else if (metrics.current_ratio > 1) score += 3;
else if (metrics.current_ratio > 0.75) score += 2;
else score += 1;
}
// 资产负债率评分
if (metrics.debt_ratio !== null && metrics.debt_ratio !== undefined) {
factors++;
if (metrics.debt_ratio < 30) score += 5;
else if (metrics.debt_ratio < 40) score += 4;
else if (metrics.debt_ratio < 50) score += 3;
else if (metrics.debt_ratio < 60) score += 2;
else if (metrics.debt_ratio < 70) score += 1;
}
// 营收增长率评分
if (metrics.revenue_growth !== null && metrics.revenue_growth !== undefined) {
factors++;
if (metrics.revenue_growth > 30) score += 5;
else if (metrics.revenue_growth > 20) score += 4;
else if (metrics.revenue_growth > 10) score += 3;
else if (metrics.revenue_growth > 5) score += 2;
else if (metrics.revenue_growth > 0) score += 1;
}
if (factors === 0) return null;
const avgScore = (score / factors) * 20; // 转换为100分制
return {
score: avgScore,
level: avgScore >= 80 ? '优秀' : avgScore >= 60 ? '良好' : avgScore >= 40 ? '一般' : '较差',
color: avgScore >= 80 ? 'green' : avgScore >= 60 ? 'blue' : avgScore >= 40 ? 'yellow' : 'red'
};
},
/**
* 格式化数据表格列配置
* @param {string} type - 表格类型
*/
getTableColumns: (type) => {
const columnConfigs = {
balanceSheet: [
{ key: 'period', label: '报告期', align: 'left' },
{ key: 'total_assets', label: '总资产', align: 'right', format: 'largeNumber' },
{ key: 'total_liabilities', label: '总负债', align: 'right', format: 'largeNumber' },
{ key: 'total_equity', label: '股东权益', align: 'right', format: 'largeNumber' },
{ key: 'current_ratio', label: '流动比率', align: 'right', format: 'decimal' },
{ key: 'debt_ratio', label: '资产负债率', align: 'right', format: 'percent' },
],
incomeStatement: [
{ key: 'period', label: '报告期', align: 'left' },
{ key: 'revenue', label: '营业收入', align: 'right', format: 'largeNumber' },
{ key: 'operating_profit', label: '营业利润', align: 'right', format: 'largeNumber' },
{ key: 'net_profit', label: '净利润', align: 'right', format: 'largeNumber' },
{ key: 'gross_margin', label: '毛利率', align: 'right', format: 'percent' },
{ key: 'net_margin', label: '净利率', align: 'right', format: 'percent' },
],
cashflow: [
{ key: 'period', label: '报告期', align: 'left' },
{ key: 'operating_cashflow', label: '经营现金流', align: 'right', format: 'largeNumber' },
{ key: 'investing_cashflow', label: '投资现金流', align: 'right', format: 'largeNumber' },
{ key: 'financing_cashflow', label: '筹资现金流', align: 'right', format: 'largeNumber' },
{ key: 'free_cashflow', label: '自由现金流', align: 'right', format: 'largeNumber' },
],
metrics: [
{ key: 'period', label: '报告期', align: 'left' },
{ key: 'roe', label: 'ROE', align: 'right', format: 'percent' },
{ key: 'roa', label: 'ROA', align: 'right', format: 'percent' },
{ key: 'eps', label: 'EPS', align: 'right', format: 'decimal' },
{ key: 'pe', label: 'PE', align: 'right', format: 'decimal' },
{ key: 'pb', label: 'PB', align: 'right', format: 'decimal' },
]
};
return columnConfigs[type] || [];
}
};
// 图表数据处理工具
export const chartUtils = {
/**
* 准备趋势图数据
* @param {array} data - 原始数据
* @param {array} metrics - 要显示的指标
*/
prepareTrendData: (data, metrics) => {
return data.map(item => {
const point = {
period: formatUtils.getReportType(item.period)
};
metrics.forEach(metric => {
point[metric.key] = item[metric.dataPath];
});
return point;
}).reverse(); // 按时间顺序排列
},
/**
* 准备饼图数据
* @param {array} data - 原始数据
* @param {string} valueKey - 值字段
* @param {string} nameKey - 名称字段
*/
preparePieData: (data, valueKey, nameKey) => {
const total = data.reduce((sum, item) => sum + (item[valueKey] || 0), 0);
return data.map(item => ({
name: item[nameKey],
value: item[valueKey],
percentage: total > 0 ? (item[valueKey] / total * 100).toFixed(2) : 0
}));
},
/**
* 准备对比柱状图数据
* @param {array} data - 原始数据
* @param {array} periods - 要对比的期间
* @param {array} metrics - 要对比的指标
*/
prepareComparisonData: (data, periods, metrics) => {
return metrics.map(metric => ({
metric: metric.label,
...periods.reduce((acc, period, index) => {
const periodData = data.find(d => d.period === period);
acc[`period${index + 1}`] = periodData ? periodData[metric.key] : 0;
return acc;
}, {})
}));
},
/**
* 获取图表颜色配置
* @param {string} theme - 主题类型
*/
getChartColors: (theme = 'default') => {
const themes = {
default: ['#3182CE', '#38A169', '#DD6B20', '#805AD5', '#D69E2E', '#E53E3E'],
blue: ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D'],
green: ['#38A169', '#48BB78', '#68D391', '#9AE6B4', '#C6F6D5', '#F0FFF4']
};
return themes[theme] || themes.default;
}
};
export default financialService;