项目原型
text
📄 PDF智能公式计算系统
==================================================
[文件处理面板]
已上传文件: 2023年度财务报告.pdf
处理状态: ✅ 完成
提取结果: 表格(15个) | 公式(42个) | 文本页(28页)
[财务比率分析]
┌─────────────────┬──────────┬──────────────┐
│ 比率名称 │ 计算值 │ 状态 │
├─────────────────┼──────────┼──────────────┤
│ 流动比率 │ 1.25 ✅ 正常 │
│ 资产负债率 │ 68.5% ⚠️ 偏高 │
│ 净利润率 │ 8.2% ✅ 正常 │
│ 净资产收益率 │ 12.5% ✅ 良好 │
└─────────────────┴──────────┴──────────────┘
[公式计算详情]
┌──────────────────────────────────────────────┐
│ 公式: 总资产周转率 = 营业收入 / 平均总资产 │
│ 计算结果: 0.85 次/年 │
│ 数据来源: 利润表第3行, 资产负债表第15行 │
│ 置信度: ██████████ 92% │
└──────────────────────────────────────────────┘
[风险评估结果]
🔴 综合风险等级: 中等风险 (得分: 58/100)
⚠️ 主要风险点:
• 资产负债率超过65%,存在偿债压力
• 存货周转率偏低,库存管理待优化
• 经营活动现金流为负
💡 改进建议:
• 优化资本结构,降低有息负债
• 加强存货管理,提高周转效率
• 改善现金流管理,确保经营安全
[数据验证面板]
▢ 自动验证计算结果 ▢ 人工复核关键指标
▢ 生成审计轨迹 ▢ 导出计算明细
[操作选项]
1. 📊 生成详细报告 2. 💾 导出Excel
3. 📈 趋势分析 4. ⚠️ 风险预警设置
5. 🔍 公式追溯 6. ❓ 帮助
请输入选择 [1-6]:
配置参数
yml
# config/pdf_formula_config.yaml
ocr:
tesseract_path: "/usr/bin/tesseract"
resolution: 300
languages: ["chi_sim", "eng"]
psm_mode: 6
formula_detection:
latex_patterns:
- '\$[^$]+\$'
- '\\\[.*?\\\]'
- '\\begin{equation}.*?\\end{equation}'
financial_patterns:
- '[资产|负债|收入|利润|成本].*?[:\=].*?[\d\.,]+'
calculation:
default_precision: 4
safe_eval: true
max_formula_complexity: 100
financial_analysis:
risk_thresholds:
current_ratio: 1.0
debt_to_equity: 1.0
net_profit_margin: 0.05
ratio_weights:
liquidity: 0.3
solvency: 0.4
profitability: 0.3
output:
report_format: "html"
include_charts: true
save_intermediate: false
核心代码
python
import pdfplumber
import pytesseract
from PIL import Image
import cv2
import numpy as np
import pandas as pd
import re
from typing import Dict,List,Tuple,Any
import sympy as sp
from sympy.parsing.latex import parse_latex
import logging
from dataclasses import dataclass
import io
@dataclass
class FormulaResult:
"""公式计算结果数据类"""
formula_text:str
formula_type:str
variables: Dict[str, float]
calculated_value: float
confidence: float
source_location: Tuple[int, int] # (page, bbox)
class PDFFormulaCalculator:
"""
PDF智能公式与计算引擎,处理非结构化PDF中的表格、文本和数学公式
"""
def __init__(self,config:Dict):
self.config = config
self.setup_logging()
# 初始化OCR引擎
self.ocr_config = config.get('ocr', {})
pytesseract.pytesseract.tesseract_cmd = self.ocr_config.get('tesseract_path', '/usr/bin/tesseract')
# 数学公式识别模式
self.formula_patterns = {
'latex_inline': re.compile(r'\$([^$]+)\$'),
'latex_display': re.compile(r'\\\[(.+?)\\\]'),
'simple_arithmetic': re.compile(r'([\d\.]+\s*[\+\-\*\/]\s*[\d\.]+)'),
'financial_ratio': re.compile(r'([A-Za-z]+\s*[\:\=]\s*[\d\.]+\s*[\/\%])'),
}
# 财务指标公式库
self.financial_formulas = {
'current_ratio': lambda ca, cl: ca / cl,
'debt_to_equity': lambda td, te: td / te,
'gross_margin': lambda revenue, cogs: (revenue - cogs) / revenue,
'net_profit_margin': lambda net_income, revenue: net_income / revenue,
'roe': lambda net_income, equity: net_income / equity,
'roa': lambda net_income, total_assets: net_income / total_assets
}
# 符号计算变量
self.symbols = {}
def setup_logging(self):
"""配置日志系统"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('pdf_formula_calculator.log'),
logging.StreamHandler()
]
)
self.logger = logging.getLogger(__name__)
def frocess_financial_report(self, pdf_path: str)->Dict[str,Any]:
"""处理财务报表PDF,提取并计算所有公式"""
self.logger.info(f"开始处理财务报表:{pdf_path}")
results = {
'metadata': {},
'extracted_tables': [],
'identified_formulas': [],
'calculated_values': [],
'financial_ratios': [],
'risk_assessment': {}
}
try:
with pdfplumber.open(pdf_path) as pdf:
results['metadata']['total_pages'] = len(pdf.pages)
results['metadata']['file_name'] = pdf_path
for page_num,page in enumerate(pdf.pages):
self.logger.info(f"处理第 {page_num + 1} 页")
#提取文本内容
text_content = page.extract_text()
if text_content:
#识别文本中的公式
text_formulas = self._extract_formulas_from_Text(text_content,page_num)
results['identified_formulas'].extend(text_formulas)
#提取表格数据
tables = page.extract_tables()
for table_num,table in enumerate(tables):
table_data = self._process_table(table, page_num, table_num)
if table_data:
results['extracted_tables'].append(table_data)
# 从表格中识别公式
table_formulas = self._extract_formulas_from_table(table_data)
results['identified_formulas'].extend(table_formulas)
# OCR处理图像区域(用于识别扫描的公式)
ocr_results = self._process_images_with_ocr(page, page_num)
results['identified_formulas'].extend(ocr_results)
# 执行公式计算
calculation_results = self._calculate_all_formulas(results['identified_formulas'])
results['calculated_values'] = calculation_results
# 计算财务比率
financial_ratios = self._calculate_financial_ratios(results)
results['financial_ratios'] = financial_ratios
# 风险评估
risk_assessment = self._perform_risk_assessment(financial_ratios)
results['risk_assessment'] = risk_assessment
self.logger.info("财务报表处理完成")
except Exception as e:
self.logger.error(f"处理财务报表失败: {e}")
raise
return results
def _extract_formulas_from_text()->List[Dict]:
"""从文本中提取公式"""
formulas = []
#识别LaTex公式
for pattern_name,pattern in self.formula_patterns.items()"
matches = pattern.findall(text)
for match in matches:
formula_info = {
'type': pattern_name,
'formula': match,
'source': 'text',
'page': page_num,
'confidence': 0.9,
'context': text[:100] # 上下文信息
}
formulas.append(formula_info)
# 识别财务指标表达式
financial_indicators = self._identify_financial_indicators(text)
formulas.extend(financial_indicators)
return formulas
def _identify_financial_indicators(self, text: str) -> List[Dict]:
"""识别财务指标"""
indicators = []
# 财务指标关键词
financial_keywords = {
'资产负债率': 'debt_ratio',
'流动比率': 'current_ratio',
'速动比率': 'quick_ratio',
'净利润率': 'net_profit_margin',
'毛利率': 'gross_margin',
'净资产收益率': 'roe',
'总资产收益率': 'roa'
}
for chinese_name, english_name in financial_keywords.items():
if chinese_name in text:
# 提取数值
value_pattern = re.compile(f'{chinese_name}[::]?\\s*([\\d\\.]+)')
value_match = value_pattern.search(text)
if value_match:
indicators.append({
'type': 'financial_indicator',
'name': english_name,
'chinese_name': chinese_name,
'value': float(value_match.group(1)),
'source': 'text',
'confidence': 0.85
})
return indicators
def _process_table(self, table_data: List, page_num: int, table_num: int) -> Dict:
"""处理表格数据"""
if not table_data or len(table_data) < 2:
return None
# 清理表格数据
cleaned_table = []
for row in table_data:
cleaned_row = []
for cell in row:
if cell is None:
cleaned_row.append('')
else:
# 清理单元格文本
cell_text = re.sub(r'\s+', ' ', str(cell)).strip()
cleaned_row.append(cell_text)
cleaned_table.append(cleaned_row)
# 识别表头
headers = cleaned_table[0]
data_rows = cleaned_table[1:]
table_info = {
'page': page_num,
'table_number': table_num,
'headers': headers,
'data': data_rows,
'shape': (len(data_rows), len(headers))
}
return table_info
def _extract_formulas_from_table(self, table_data: Dict) -> List[Dict]:
"""从表格中提取公式"""
formulas = []
headers = table_data['headers']
data = table_data['data']
# 检查表格是否包含财务数据
financial_headers = ['金额', '数值', '比率', '比例', '率']
is_financial_table = any(any(fh in header for fh in financial_headers) for header in headers)
if is_financial_table:
# 识别计算关系
for i, row in enumerate(data):
for j, cell in enumerate(row):
if cell and any(op in cell for op in ['+', '-', '*', '/', '=', ':']):
formula_info = {
'type': 'table_calculation',
'formula': cell,
'source': 'table',
'page': table_data['page'],
'table': table_data['table_number'],
'row': i + 1,
'column': j + 1,
'confidence': 0.8
}
formulas.append(formula_info)
return formulas
def _process_images_with_ocr(self, page, page_num: int) -> List[Dict]:
"""使用OCR处理图像中的公式"""
formulas = []
try:
# 提取页面图像
page_image = page.to_image(resolution=300)
image_array = np.array(page_image.original)
# 转换为灰度图像
gray_image = cv2.cvtColor(image_array, cv2.COLOR_RGB2GRAY)
# 二值化处理
_, binary_image = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 使用OCR识别文本
ocr_text = pytesseract.image_to_string(
binary_image,
config='--psm 6 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+-*/=(){}[]'
)
# 从OCR结果中提取公式
ocr_formulas = self._extract_formulas_from_text(ocr_text, page_num)
for formula in ocr_formulas:
formula['source'] = 'ocr'
formula['confidence'] *= 0.8 # OCR识别置信度较低
formulas.append(formula)
except Exception as e:
self.logger.warning(f"OCR处理失败: {e}")
return formulas
def _calculate_all_formulas(self, formulas: List[Dict]) -> List[FormulaResult]:
"""计算所有识别到的公式"""
results = []
for formula_info in formulas:
try:
if formula_info['type'] == 'financial_indicator':
# 财务指标直接记录
result = FormulaResult(
formula_text=formula_info['chinese_name'],
formula_type='financial_indicator',
variables={},
calculated_value=formula_info['value'],
confidence=formula_info['confidence'],
source_location=(formula_info.get('page', 0), 0)
)
results.append(result)
elif formula_info['type'] in ['latex_inline', 'latex_display']:
# LaTeX公式计算
latex_result = self._calculate_latex_formula(formula_info['formula'])
if latex_result:
results.append(latex_result)
elif formula_info['type'] == 'simple_arithmetic':
# 简单算术计算
arithmetic_result = self._calculate_arithmetic(formula_info['formula'])
if arithmetic_result:
results.append(arithmetic_result)
elif formula_info['type'] == 'table_calculation':
# 表格计算
table_result = self._calculate_table_formula(formula_info)
if table_result:
results.append(table_result)
except Exception as e:
self.logger.warning(f"公式计算失败 {formula_info}: {e}")
continue
return results
def _calculate_latex_formula(self, latex_formula: str) -> FormulaResult:
"""计算LaTeX公式"""
try:
# 简化LaTeX解析(实际项目应使用更完整的解析器)
# 替换常见的LaTeX命令
cleaned_formula = latex_formula.replace('\\frac', '').replace('{', '(').replace('}', ')')
cleaned_formula = re.sub(r'\\[a-zA-Z]+', '', cleaned_formula)
# 提取变量和数值
variables = {}
numbers = re.findall(r'[A-Za-z]+', cleaned_formula)
for var in numbers:
if var not in variables and len(var) == 1: # 单字母变量
variables[var] = 1.0 # 默认值
# 符号计算
if variables:
expr = sp.sympify(cleaned_formula)
# 这里简化处理,实际应该从文档中提取变量值
calculated_value = float(expr.subs(variables))
else:
calculated_value = float(sp.sympify(cleaned_formula))
return FormulaResult(
formula_text=latex_formula,
formula_type='latex',
variables=variables,
calculated_value=calculated_value,
confidence=0.7,
source_location=(0, 0)
)
except Exception as e:
self.logger.warning(f"LaTeX公式计算失败 {latex_formula}: {e}")
return None
def _calculate_arithmetic(self, arithmetic_expr: str) -> FormulaResult:
"""计算算术表达式"""
try:
# 清理表达式
cleaned_expr = re.sub(r'[^\d\.\+\-\*\/\(\)]', '', arithmetic_expr)
# 安全评估
if self._is_safe_expression(cleaned_expr):
calculated_value = eval(cleaned_expr)
return FormulaResult(
formula_text=arithmetic_expr,
formula_type='arithmetic',
variables={},
calculated_value=calculated_value,
confidence=0.95,
source_location=(0, 0)
)
else:
self.logger.warning(f"不安全表达式: {arithmetic_expr}")
return None
except Exception as e:
self.logger.warning(f"算术计算失败 {arithmetic_expr}: {e}")
return None
def _is_safe_expression(self, expr: str) -> bool:
"""检查表达式是否安全"""
dangerous_patterns = [
r'__', r'import', r'eval', r'exec', r'open', r'file',
r'os\.', r'sys\.', r'subprocess'
]
for pattern in dangerous_patterns:
if re.search(pattern, expr):
return False
return True
def _calculate_table_formula(self, formula_info: Dict) -> FormulaResult:
"""计算表格中的公式"""
try:
formula_text = formula_info['formula']
# 提取变量和操作符
if '=' in formula_text:
# 类似 "A = B + C" 的公式
parts = formula_text.split('=')
if len(parts) == 2:
left_side = parts[0].strip()
right_side = parts[1].strip()
# 计算右侧表达式
calculated_value = self._calculate_arithmetic(right_side).calculated_value
return FormulaResult(
formula_text=formula_text,
formula_type='table_assignment',
variables={left_side: calculated_value},
calculated_value=calculated_value,
confidence=formula_info['confidence'],
source_location=(formula_info['page'], formula_info['table'])
)
return None
except Exception as e:
self.logger.warning(f"表格公式计算失败 {formula_info}: {e}")
return None
def _calculate_financial_ratios(self, results: Dict) -> List[Dict]:
"""计算财务比率"""
ratios = []
# 从提取的数据中收集财务数值
financial_data = self._extract_financial_data(results)
# 计算标准财务比率
for ratio_name, ratio_func in self.financial_formulas.items():
try:
ratio_value = self._calculate_specific_ratio(ratio_name, ratio_func, financial_data)
if ratio_value is not None:
ratios.append({
'ratio_name': ratio_name,
'value': ratio_value,
'interpretation': self._interpret_ratio(ratio_name, ratio_value)
})
except Exception as e:
self.logger.warning(f"财务比率计算失败 {ratio_name}: {e}")
continue
return ratios
def _extract_financial_data(self, results: Dict) -> Dict[str, float]:
"""从结果中提取财务数据"""
financial_data = {}
# 从表格中提取数据
for table in results['extracted_tables']:
for i, row in enumerate(table['data']):
for j, cell in enumerate(row):
if cell and j < len(table['headers']):
header = table['headers'][j]
# 识别财务相关列
if any(keyword in header for keyword in ['资产', '负债', '收入', '利润', '成本']):
# 提取数值
numbers = re.findall(r'[\d\.,]+', cell)
if numbers:
try:
# 清理数值字符串
clean_number = numbers[0].replace(',', '')
value = float(clean_number)
financial_data[f"{header}_{i}"] = value
except ValueError:
continue
# 从计算值中提取数据
for calc in results['calculated_values']:
if '资产' in calc.formula_text or '负债' in calc.formula_text:
financial_data[calc.formula_text] = calc.calculated_value
return financial_data
def _calculate_specific_ratio(self, ratio_name: str, ratio_func, financial_data: Dict) -> float:
"""计算特定财务比率"""
# 根据比率名称选择所需的输入参数
parameter_mapping = {
'current_ratio': ['current_assets', 'current_liabilities'],
'debt_to_equity': ['total_debt', 'total_equity'],
'gross_margin': ['revenue', 'cost_of_goods_sold'],
'net_profit_margin': ['net_income', 'revenue'],
'roe': ['net_income', 'shareholders_equity'],
'roa': ['net_income', 'total_assets']
}
if ratio_name in parameter_mapping:
params = parameter_mapping[ratio_name]
param_values = []
for param in params:
# 在财务数据中查找匹配的参数值
found_value = None
for key, value in financial_data.items():
if param in key.lower().replace(' ', '_'):
found_value = value
break
if found_value is None:
# 使用默认值或估算值
found_value = 1.0 # 简化处理
param_values.append(found_value)
return ratio_func(*param_values)
return None
def _interpret_ratio(self, ratio_name: str, ratio_value: float) -> str:
"""解释财务比率"""
interpretations = {
'current_ratio': {
'range': [(0, 1), (1, 2), (2, float('inf'))],
'messages': ['流动性风险高', '流动性适中', '流动性良好']
},
'debt_to_equity': {
'range': [(0, 0.5), (0.5, 1), (1, float('inf'))],
'messages': ['负债水平低', '负债水平适中', '负债水平高']
},
'net_profit_margin': {
'range': [(0, 0.05), (0.05, 0.15), (0.15, float('inf'))],
'messages': ['盈利能力较弱', '盈利能力适中', '盈利能力强劲']
}
}
if ratio_name in interpretations:
ranges = interpretations[ratio_name]['range']
messages = interpretations[ratio_name]['messages']
for i, (low, high) in enumerate(ranges):
if low <= ratio_value < high:
return messages[i]
return "需要进一步分析"
def _perform_risk_assessment(self, financial_ratios: List[Dict]) -> Dict:
"""执行风险评估"""
risk_score = 0
warnings = []
recommendations = []
for ratio in financial_ratios:
ratio_name = ratio['ratio_name']
value = ratio['value']
if ratio_name == 'current_ratio' and value < 1.0:
risk_score += 30
warnings.append("流动比率低于1,存在短期偿债风险")
recommendations.append("建议加强现金流管理")
elif ratio_name == 'debt_to_equity' and value > 1.0:
risk_score += 25
warnings.append("资产负债率过高,财务风险较大")
recommendations.append("建议优化资本结构,降低负债")
elif ratio_name == 'net_profit_margin' and value < 0.05:
risk_score += 20
warnings.append("净利润率偏低,盈利能力不足")
recommendations.append("建议控制成本或提升收入")
# 确定风险等级
if risk_score >= 50:
risk_level = "高风险"
elif risk_score >= 25:
risk_level = "中等风险"
else:
risk_level = "低风险"
return {
'risk_score': risk_score,
'risk_level': risk_level,
'warnings': warnings,
'recommendations': recommendations
}
def generate_report(self, results: Dict, output_format: str = 'html') -> str:
"""生成分析报告"""
if output_format == 'html':
return self._generate_html_report(results)
else:
return self._generate_text_report(results)
def _generate_html_report(self, results: Dict) -> str:
"""生成HTML格式报告"""
html_template = """
<!DOCTYPE html>
<html>
<head>
<title>财务报表智能分析报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.header { background: #f4f4f4; padding: 20px; border-radius: 5px; }
.section { margin: 20px 0; padding: 15px; border-left: 4px solid #007cba; }
.warning { background: #fff3cd; border-color: #ffc107; }
.success { background: #d1ecf1; border-color: #0c5460; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<div class="header">
<h1>📊 财务报表智能分析报告</h1>
<p>文件: {file_name} | 页数: {total_pages} | 生成时间: {timestamp}</p>
</div>
<div class="section">
<h2>📈 财务比率分析</h2>
<table>
<tr><th>比率名称</th><th>数值</th><th>解读</th></tr>
{ratios_table}
</table>
</div>
<div class="section {risk_class}">
<h2>⚠️ 风险评估</h2>
<p><strong>风险等级:</strong> {risk_level} (得分: {risk_score}/100)</p>
<h3>警告事项:</h3>
<ul>{warnings_list}</ul>
<h3>改进建议:</h3>
<ul>{recommendations_list}</ul>
</div>
<div class="section">
<h2>🧮 公式计算详情</h2>
<table>
<tr><th>公式</th><th>类型</th><th>计算结果</th><th>置信度</th></tr>
{calculations_table}
</table>
</div>
</body>
</html>
"""
# 填充模板
from datetime import datetime
ratios_rows = ""
for ratio in results['financial_ratios']:
ratios_rows += f"<tr><td>{ratio['ratio_name']}</td><td>{ratio['value']:.4f}</td><td>{ratio['interpretation']}</td></tr>"
calculations_rows = ""
for calc in results['calculated_values'][:10]: # 显示前10个计算结果
calculations_rows += f"<tr><td>{calc.formula_text}</td><td>{calc.formula_type}</td><td>{calc.calculated_value:.2f}</td><td>{calc.confidence:.2f}</td></tr>"
warnings_list = "".join([f"<li>{warning}</li>" for warning in results['risk_assessment']['warnings']])
recommendations_list = "".join([f"<li>{rec}</li>" for rec in results['risk_assessment']['recommendations']])
risk_class = "warning" if results['risk_assessment']['risk_score'] > 25 else "success"
filled_html = html_template.format(
file_name=results['metadata']['file_name'],
total_pages=results['metadata']['total_pages'],
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
ratios_table=ratios_rows,
calculations_table=calculations_rows,
risk_level=results['risk_assessment']['risk_level'],
risk_score=results['risk_assessment']['risk_score'],
warnings_list=warnings_list,
recommendations_list=recommendations_list,
risk_class=risk_class
)
return filled_html
==========================================================
Java架构
PDF智能处理在财务审计的应用
业务场景:会计师事务所从企业财务报表PDF中自动提取数据并执行审计分析
text
┌─ 智能审计分析平台 ───────────────────────────────────────┐
│ 客户: 某上市公司 | 审计期间: 2023年度 | 进度: 85% │
├─────────────────────────────────────────────────────────┤
│ 【PDF上传与解析】 │
│ 📄 2023年报.pdf ✅ 解析完成 │
│ 📄 2023审计报告.pdf ✅ 解析完成 │
│ 📄 2023合并报表.pdf ⏳ 解析中... │
│ │
│ 【自动提取关键数据】 │
│ 资产负债表: │
│ ├─ 资产总计: ¥15,238,450,000 ✅ 已核对 │
│ ├─ 负债合计: ¥8,456,230,000 ✅ 已核对 │
│ └─ 所有者权益: ¥6,782,220,000 ✅ 已核对 │
│ │
│ 利润表: │
│ ├─ 营业收入: ¥12,567,890,000 ⚠️ 需要复核 │
│ ├─ 净利润: ¥1,234,560,000 ✅ 已核对 │
│ └─ 毛利率: 35.2% 📈 同比+2.1% │
│ │
│ 【财务比率自动计算】 │
│ 偿债能力: 资产负债率 55.5% | 流动比率 1.8 | 速动比率 1.2│
│ 盈利能力: 净利率 9.8% | ROE 18.2% | ROA 8.1% │
│ 运营能力: 应收账款周转率 6.5 | 存货周转率 4.2 │
│ │
│ 【异常检测】 │
│ ⚠️ 其他应收款同比增加150%,需要重点审计 │
│ ⚠️ 销售费用率下降但营收增长,需要合理性分析 │
│ │
│ [生成审计底稿] [标记风险点] [导出调整分录] │
└─────────────────────────────────────────────────────────┘
环境搭建
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.company</groupId>
<artifactId>pdf-intelligence-system</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>PDF Intelligence System</name>
<description>企业级PDF智能解析与计算系统</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- PDF处理相关版本 -->
<pdfbox.version>3.0.1</pdfbox.version>
<tess4j.version>5.8.0</tess4j.version>
<opencv.version>4.8.1-1</opencv.version>
<poi.version>5.2.4</poi.version>
<!-- AI/ML相关版本 -->
<tensorflow.version>2.13.0</tensorflow.version>
<opennlp.version>2.2.0</opennlp.version>
<!-- 工具类版本 -->
<apache.commons.version>2.14.0</apache.commons.version>
<hutool.version>5.8.22</hutool.version>
<guava.version>32.1.3-jre</guava.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.4.6</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- PDF处理核心依赖 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox-tools</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>${tess4j.version}</version>
</dependency>
<!-- OCR和图像处理 -->
<dependency>
<groupId>org.openpnp</groupId>
<artifactId>opencv</artifactId>
<version>${opencv.version}</version>
</dependency>
<!-- 表格处理 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- AI/机器学习 -->
<dependency>
<groupId>org.tensorflow</groupId>
<artifactId>tensorflow-core-api</artifactId>
<version>${tensorflow.version}</version>
</dependency>
<dependency>
<groupId>org.apache.opennlp</groupId>
<artifactId>opennlp-tools</artifactId>
<version>${opennlp.version}</version>
</dependency>
<!-- 公式计算引擎 -->
<dependency>
<groupId>com.fathzer</groupId>
<artifactId>javaluator</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.mariuszgromada.math</groupId>
<artifactId>MathParser.org-mXparser</artifactId>
<version>5.2.1</version>
</dependency>
<!-- 工具类库 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${apache.commons.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- 文档生成 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>8.0.1</version>
<type>pom</type>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
java
package com.company.pdfsystem.entity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* PDF文档实体 - 存储上传的PDF文档元数据
* 对应数据库表:pdf_documents
*/
@Entity
@Table(name = "pdf_documents", indexes = {
@Index(name = "idx_document_hash", columnList = "fileHash"),
@Index(name = "idx_upload_time", columnList = "uploadTime"),
@Index(name = "idx_client_year", columnList = "clientName, fiscalYear")
})
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = {"extractedData", "formulas", "auditReports"})
public class PdfDocument {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 业务唯一标识
*/
@Column(nullable = false, unique = true, length = 64)
private String documentId;
/**
* 文档名称
*/
@Column(nullable = false, length = 500)
private String fileName;
/**
* 原始文件名
*/
@Column(nullable = false, length = 500)
private String originalFileName;
/**
* 文件存储路径
*/
@Column(nullable = false, length = 1000)
private String filePath;
/**
* 文件大小(字节)
*/
@Column(nullable = false)
private Long fileSize;
/**
* 文件MD5哈希值,用于去重
*/
@Column(nullable = false, length = 32)
private String fileHash;
/**
* 文档类型:财务报表、审计报告、合同等
*/
@Column(nullable = false, length = 50)
private String documentType;
/**
* 客户名称
*/
@Column(nullable = false, length = 200)
private String clientName;
/**
* 会计年度
*/
@Column(nullable = false, length = 10)
private String fiscalYear;
/**
* 文档状态:UPLOADED, PARSING, PARSED, ERROR
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private DocumentStatus status;
/**
* 总页数
*/
private Integer totalPages;
/**
* 解析进度(0-100)
*/
@Column(nullable = false)
private Integer parseProgress;
/**
* 解析错误信息
*/
@Column(columnDefinition = "TEXT")
private String errorMessage;
/**
* 上传用户ID
*/
@Column(nullable = false, length = 50)
private String uploadedBy;
/**
* 上传时间
*/
@Column(nullable = false)
private LocalDateTime uploadTime;
/**
* 解析完成时间
*/
private LocalDateTime parseCompletedTime;
/**
* 提取的数据列表
*/
@OneToMany(mappedBy = "document", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<ExtractedData> extractedData = new ArrayList<>();
/**
* 识别的公式列表
*/
@OneToMany(mappedBy = "document", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Formula> formulas = new ArrayList<>();
/**
* 审计报告列表
*/
@OneToMany(mappedBy = "document", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<AuditReport> auditReports = new ArrayList<>();
@CreationTimestamp
private LocalDateTime createTime;
@UpdateTimestamp
private LocalDateTime updateTime;
/**
* 文档状态枚举
*/
public enum DocumentStatus {
UPLOADED, // 已上传
PARSING, // 解析中
PARSED, // 解析完成
ERROR, // 解析错误
PROCESSED // 处理完成(包含计算和审计)
}
}
/**
* 提取数据实体 - 存储从PDF中提取的结构化数据
* 对应数据库表:extracted_data
*/
@Entity
@Table(name = "extracted_data", indexes = {
@Index(name = "idx_doc_page", columnList = "document_id, pageNumber"),
@Index(name = "idx_data_type", columnList = "dataType"),
@Index(name = "idx_table_ref", columnList = "tableReference")
})
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExtractedData {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 关联的PDF文档
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "document_id", nullable = false)
private PdfDocument document;
/**
* 数据所在页码
*/
@Column(nullable = false)
private Integer pageNumber;
/**
* 数据类型:TABLE, TEXT, NUMBER, FORMULA, HEADER
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private DataType dataType;
/**
* 数据内容(JSON格式)
*/
@Column(columnDefinition = "JSON")
private String dataContent;
/**
* 原始文本内容
*/
@Column(columnDefinition = "TEXT")
private String rawText;
/**
* 置信度(0-1)
*/
@Column(precision = 3, scale = 2)
private BigDecimal confidence;
/**
* 表格引用(如果是表格数据)
*/
@Column(length = 100)
private String tableReference;
/**
* 行索引
*/
private Integer rowIndex;
/**
* 列索引
*/
private Integer columnIndex;
/**
* 坐标信息(JSON格式:x1,y1,x2,y2)
*/
@Column(length = 100)
private String coordinates;
@CreationTimestamp
private LocalDateTime createTime;
/**
* 数据类型枚举
*/
public enum DataType {
TABLE, // 表格数据
TEXT, // 文本数据
NUMBER, // 数值数据
FORMULA, // 公式
HEADER, // 表头
FOOTNOTE, // 脚注
DATE, // 日期
CURRENCY // 货币金额
}
}
/**
* 公式实体 - 存储识别出的公式及其计算结果
* 对应数据库表:formulas
*/
@Entity
@Table(name = "formulas", indexes = {
@Index(name = "idx_formula_doc", columnList = "document_id"),
@Index(name = "idx_formula_type", columnList = "formulaType"),
@Index(name = "idx_formula_ref", columnList = "formulaReference")
})
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Formula {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 关联的PDF文档
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "document_id", nullable = false)
private PdfDocument document;
/**
* 公式标识符
*/
@Column(nullable = false, length = 100)
private String formulaId;
/**
* 公式类型:FINANCIAL_RATIO, CALCULATION, DERIVATION
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 30)
private FormulaType formulaType;
/**
* 原始公式文本
*/
@Column(nullable = false, columnDefinition = "TEXT")
private String originalFormula;
/**
* 标准化公式
*/
@Column(columnDefinition = "TEXT")
private String normalizedFormula;
/**
* 公式描述
*/
@Column(length = 500)
private String description;
/**
* 计算结果
*/
@Column(precision = 20, scale = 6)
private BigDecimal calculatedValue;
/**
* 期望值(如果文档中提供)
*/
@Column(precision = 20, scale = 6)
private BigDecimal expectedValue;
/**
* 偏差率
*/
@Column(precision = 8, scale = 6)
private BigDecimal deviationRate;
/**
* 计算状态:PENDING, CALCULATED, ERROR
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private CalculationStatus calculationStatus;
/**
* 错误信息
*/
@Column(columnDefinition = "TEXT")
private String errorMessage;
/**
* 公式引用(如:资产负债表公式1)
*/
@Column(length = 200)
private String formulaReference;
/**
* 依赖的变量(JSON格式)
*/
@Column(columnDefinition = "JSON")
private String dependencies;
@CreationTimestamp
private LocalDateTime createTime;
/**
* 公式类型枚举
*/
public enum FormulaType {
FINANCIAL_RATIO, // 财务比率
ARITHMETIC, // 算术计算
FINANCIAL_STATEMENT, // 财务报表公式
TAX_CALCULATION, // 税务计算
DEPRECIATION, // 折旧计算
CUSTOM // 自定义公式
}
/**
* 计算状态枚举
*/
public enum CalculationStatus {
PENDING, // 待计算
CALCULATED, // 已计算
ERROR, // 计算错误
VERIFIED // 已验证
}
}
/**
* 审计报告实体 - 存储生成的审计分析报告
* 对应数据库表:audit_reports
*/
@Entity
@Table(name = "audit_reports", indexes = {
@Index(name = "idx_report_doc", columnList = "document_id"),
@Index(name = "idx_report_type", columnList = "reportType"),
@Index(name = "idx_generate_time", columnList = "generateTime")
})
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuditReport {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 报告唯一标识
*/
@Column(nullable = false, unique = true, length = 64)
private String reportId;
/**
* 关联的PDF文档
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "document_id", nullable = false)
private PdfDocument document;
/**
* 报告类型:FINANCIAL_ANALYSIS, RISK_ASSESSMENT, COMPLIANCE_CHECK
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 50)
private ReportType reportType;
/**
* 报告标题
*/
@Column(nullable = false, length = 500)
private String title;
/**
* 报告内容(JSON格式)
*/
@Column(columnDefinition = "JSON")
private String reportContent;
/**
* 生成的HTML报告
*/
@Column(columnDefinition = "MEDIUMTEXT")
private String htmlContent;
/**
* 生成的PDF报告路径
*/
@Column(length = 1000)
private String pdfPath;
/**
* 审计发现数量
*/
@Column(nullable = false)
private Integer findingCount;
/**
* 高风险数量
*/
@Column(nullable = false)
private Integer highRiskCount;
/**
* 报告状态:GENERATING, COMPLETED, ERROR
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ReportStatus status;
/**
* 生成用户
*/
@Column(nullable = false, length = 50)
private String generatedBy;
/**
* 生成时间
*/
@Column(nullable = false)
private LocalDateTime generateTime;
/**
* 完成时间
*/
private LocalDateTime completedTime;
@CreationTimestamp
private LocalDateTime createTime;
/**
* 报告类型枚举
*/
public enum ReportType {
FINANCIAL_ANALYSIS, // 财务分析
RISK_ASSESSMENT, // 风险评估
COMPLIANCE_CHECK, // 合规检查
INTERNAL_CONTROL, // 内部控制
TAX_COMPLIANCE, // 税务合规
CUSTOM_ANALYSIS // 自定义分析
}
/**
* 报告状态枚举
*/
public enum ReportStatus {
GENERATING, // 生成中
COMPLETED, // 已完成
ERROR, // 生成错误
PUBLISHED // 已发布
}
}
PDF解析服务实现
java
package com.company.pdfsystem.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* PDF文本提取服务
* 负责从PDF文档中提取文本内容,包括表格、公式等
*/
@Service
@Slf4j
public class PdfTextExtractionService {
private final OcrService ocrService;
private final TableDetectionService tableDetectionService;
private final FormulaRecognitionService formulaRecognitionService;
/**
* 从PDF文件中提取所有文本内容
*
* @param pdfFile 上传的PDF文件
* @return 提取的文本内容列表,按页面组织
* @throws PdfProcessingException 如果PDF处理失败
*/
public List<PageTextContent> extractAllText(MultipartFile pdfFile) throws PdfProcessingException {
log.info("开始提取PDF文本内容,文件名: {}", pdfFile.getOriginalFilename());
List<PageTextContent> pageContents = new ArrayList<>();
try (PDDocument document = PDDocument.load(pdfFile.getInputStream())) {
int totalPages = document.getNumberOfPages();
log.info("PDF文档总页数: {}", totalPages);
// 创建自定义的PDF文本提取器
CustomPDFTextStripper textStripper = new CustomPDFTextStripper();
for (int pageNum = 1; pageNum <= totalPages; pageNum++) {
log.debug("正在提取第 {} 页文本内容", pageNum);
try {
// 设置提取范围
textStripper.setStartPage(pageNum);
textStripper.setEndPage(pageNum);
// 提取文本
String pageText = textStripper.getText(document);
// 获取文本位置信息
List<TextPosition> textPositions = textStripper.getTextPositions();
// 构建页面内容对象
PageTextContent pageContent = PageTextContent.builder()
.pageNumber(pageNum)
.textContent(pageText)
.textPositions(textPositions)
.build();
pageContents.add(pageContent);
log.debug("第 {} 页文本提取完成,字符数: {}", pageNum, pageText.length());
} catch (IOException e) {
log.warn("提取第 {} 页文本失败: {}", pageNum, e.getMessage());
// 继续处理下一页
}
}
log.info("PDF文本提取完成,成功提取 {} 页", pageContents.size());
return pageContents;
} catch (IOException e) {
log.error("加载PDF文档失败: {}", e.getMessage(), e);
throw new PdfProcessingException("PDF文档加载失败: " + e.getMessage(), e);
}
}
/**
* 提取PDF中的表格数据
*
* @param pdfFile PDF文件
* @return 提取的表格数据列表
*/
public List<ExtractedTable> extractTables(MultipartFile pdfFile) {
log.info("开始提取PDF表格数据");
try (PDDocument document = PDDocument.load(pdfFile.getInputStream())) {
List<ExtractedTable> tables = new ArrayList<>();
int totalPages = document.getNumberOfPages();
for (int pageNum = 1; pageNum <= totalPages; pageNum++) {
log.debug("正在检测第 {} 页表格", pageNum);
List<ExtractedTable> pageTables = tableDetectionService.detectTables(document, pageNum);
tables.addAll(pageTables);
log.debug("第 {} 页检测到 {} 个表格", pageNum, pageTables.size());
}
log.info("表格提取完成,共检测到 {} 个表格", tables.size());
return tables;
} catch (Exception e) {
log.error("提取PDF表格失败: {}", e.getMessage(), e);
throw new PdfProcessingException("表格提取失败: " + e.getMessage(), e);
}
}
/**
* 提取PDF中的公式
*
* @param pdfFile PDF文件
* @return 提取的公式列表
*/
public List<ExtractedFormula> extractFormulas(MultipartFile pdfFile) {
log.info("开始提取PDF公式");
try (PDDocument document = PDDocument.load(pdfFile.getInputStream())) {
List<ExtractedFormula> formulas = new ArrayList<>();
int totalPages = document.getNumberOfPages();
for (int pageNum = 1; pageNum <= totalPages; pageNum++) {
log.debug("正在识别第 {} 页公式", pageNum);
List<ExtractedFormula> pageFormulas = formulaRecognitionService.recognizeFormulas(document, pageNum);
formulas.addAll(pageFormulas);
log.debug("第 {} 页识别到 {} 个公式", pageNum, pageFormulas.size());
}
log.info("公式提取完成,共识别到 {} 个公式", formulas.size());
return formulas;
} catch (Exception e) {
log.error("提取PDF公式失败: {}", e.getMessage(), e);
throw new PdfProcessingException("公式提取失败: " + e.getMessage(), e);
}
}
/**
* 使用OCR处理扫描版PDF
*
* @param pdfFile 扫描版PDF文件
* @return OCR识别结果
*/
public OcrResult processScannedPdf(MultipartFile pdfFile) {
log.info("开始处理扫描版PDF");
try {
// 将PDF转换为图像
List<BufferedImage> pageImages = convertPdfToImages(pdfFile);
OcrResult ocrResult = new OcrResult();
List<OcrPageResult> pageResults = new ArrayList<>();
for (int i = 0; i < pageImages.size(); i++) {
log.debug("正在OCR识别第 {} 页", i + 1);
OcrPageResult pageResult = ocrService.recognizeText(pageImages.get(i));
pageResult.setPageNumber(i + 1);
pageResults.add(pageResult);
log.debug("第 {} 页OCR识别完成,识别文本长度: {}",
i + 1, pageResult.getText().length());
}
ocrResult.setPageResults(pageResults);
ocrResult.setTotalPages(pageImages.size());
log.info("扫描版PDF处理完成,共处理 {} 页", pageImages.size());
return ocrResult;
} catch (Exception e) {
log.error("处理扫描版PDF失败: {}", e.getMessage(), e);
throw new PdfProcessingException("扫描版PDF处理失败: " + e.getMessage(), e);
}
}
/**
* 自定义PDF文本提取器,用于获取文本位置信息
*/
private static class CustomPDFTextStripper extends PDFTextStripper {
private final List<TextPosition> textPositions = new ArrayList<>();
public CustomPDFTextStripper() throws IOException {
super();
}
@Override
protected void writeString(String string, List<TextPosition> textPositions) throws IOException {
this.textPositions.addAll(textPositions);
super.writeString(string, textPositions);
}
public List<TextPosition> getTextPositions() {
return new ArrayList<>(textPositions);
}
}
}
/**
* 页面文本内容数据类
*/
@Data
@Builder
class PageTextContent {
private Integer pageNumber;
private String textContent;
private List<TextPosition> textPositions;
private List<TextBlock> textBlocks;
}
/**
* 文本块数据类
*/
@Data
@Builder
class TextBlock {
private String text;
private Rectangle boundingBox;
private String fontName;
private Float fontSize;
private Boolean isBold;
private Boolean isItalic;
}
/**
* 提取的表格数据类
*/
@Data
@Builder
class ExtractedTable {
private Integer pageNumber;
private Rectangle boundingBox;
private List<List<String>> rows;
private List<String> headers;
private String tableType;
private Double confidence;
}
/**
* 提取的公式数据类
*/
@Data
@Builder
class ExtractedFormula {
private Integer pageNumber;
private Rectangle boundingBox;
private String originalFormula;
private String normalizedFormula;
private String formulaType;
private Double confidence;
}
/**
* OCR识别结果数据类
*/
@Data
@Builder
class OcrResult {
private List<OcrPageResult> pageResults;
private Integer totalPages;
private String language;
private Double overallConfidence;
}
/**
* OCR页面识别结果数据类
*/
@Data
@Builder
class OcrPageResult {
private Integer pageNumber;
private String text;
private List<OcrTextBlock> textBlocks;
private Double confidence;
private Long processingTime;
}
公式计算引擎实现
java
package com.company.pdfsystem.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.mariuszgromada.math.mxparser.Expression;
import org.mariuszgromada.math.mxparser.Function;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 公式计算服务
* 负责解析和计算从PDF中提取的数学公式和财务公式
*/
@Service
@Slf4j
public class FormulaCalculationService {
private final FinancialDataService financialDataService;
private final Map<String, Double> variableCache = new HashMap<>();
// 财务比率公式定义
private static final Map<String, String> FINANCIAL_RATIO_FORMULAS = Map.of(
"current_ratio", "CurrentAssets / CurrentLiabilities",
"quick_ratio", "(CurrentAssets - Inventory) / CurrentLiabilities",
"debt_to_equity", "TotalLiabilities / TotalEquity",
"return_on_equity", "NetIncome / TotalEquity",
"gross_profit_margin", "(Revenue - COGS) / Revenue",
"net_profit_margin", "NetIncome / Revenue",
"asset_turnover", "Revenue / TotalAssets",
"inventory_turnover", "COGS / AverageInventory"
);
/**
* 计算单个公式
*
* @param formula 公式表达式
* @param variables 变量值映射
* @return 计算结果
* @throws FormulaCalculationException 如果计算失败
*/
public CalculationResult calculateFormula(String formula, Map<String, BigDecimal> variables)
throws FormulaCalculationException {
log.info("开始计算公式: {}", formula);
log.debug("公式变量: {}", variables);
try {
// 1. 标准化公式表达式
String normalizedFormula = normalizeFormula(formula);
log.debug("标准化公式: {}", normalizedFormula);
// 2. 验证公式语法
validateFormulaSyntax(normalizedFormula);
// 3. 创建表达式
Expression expression = new Expression(normalizedFormula);
// 4. 设置变量值
setExpressionVariables(expression, variables);
// 5. 计算表达式
double result = expression.calculate();
if (Double.isNaN(result) || Double.isInfinite(result)) {
throw new FormulaCalculationException("公式计算结果无效: " + result);
}
// 6. 构建计算结果
CalculationResult calculationResult = CalculationResult.builder()
.originalFormula(formula)
.normalizedFormula(normalizedFormula)
.calculatedValue(BigDecimal.valueOf(result))
.variables(new HashMap<>(variables))
.success(true)
.build();
log.info("公式计算完成: {} = {}", formula, result);
return calculationResult;
} catch (Exception e) {
log.error("公式计算失败: {}, 错误: {}", formula, e.getMessage(), e);
throw new FormulaCalculationException("公式计算失败: " + e.getMessage(), e);
}
}
/**
* 批量计算财务比率
*
* @param financialData 财务数据
* @return 财务比率计算结果
*/
public Map<String, CalculationResult> calculateFinancialRatios(FinancialData financialData) {
log.info("开始计算财务比率");
Map<String, CalculationResult> results = new HashMap<>();
Map<String, BigDecimal> variables = convertFinancialDataToVariables(financialData);
for (Map.Entry<String, String> entry : FINANCIAL_RATIO_FORMULAS.entrySet()) {
String ratioName = entry.getKey();
String formula = entry.getValue();
try {
CalculationResult result = calculateFormula(formula, variables);
results.put(ratioName, result);
log.debug("财务比率 {} 计算完成: {}", ratioName, result.getCalculatedValue());
} catch (FormulaCalculationException e) {
log.warn("计算财务比率 {} 失败: {}", ratioName, e.getMessage());
// 创建失败的结果
CalculationResult errorResult = CalculationResult.builder()
.originalFormula(formula)
.normalizedFormula(formula)
.calculatedValue(null)
.variables(variables)
.success(false)
.errorMessage(e.getMessage())
.build();
results.put(ratioName, errorResult);
}
}
log.info("财务比率计算完成,成功: {}, 失败: {}",
results.values().stream().filter(CalculationResult::isSuccess).count(),
results.values().stream().filter(r -> !r.isSuccess()).count());
return results;
}
/**
* 验证公式计算结果的合理性
*
* @param result 计算结果
* @param expectedValue 期望值
* @param tolerance 容忍偏差
* @return 验证结果
*/
public ValidationResult validateCalculation(CalculationResult result,
BigDecimal expectedValue,
BigDecimal tolerance) {
log.debug("验证计算结果,计算值: {}, 期望值: {}",
result.getCalculatedValue(), expectedValue);
if (result.getCalculatedValue() == null) {
return ValidationResult.builder()
.isValid(false)
.deviation(BigDecimal.ZERO)
.deviationRate(BigDecimal.ZERO)
.message("计算结果为空")
.build();
}
// 计算偏差
BigDecimal deviation = result.getCalculatedValue().subtract(expectedValue).abs();
BigDecimal deviationRate = deviation.divide(expectedValue.abs(), 6, RoundingMode.HALF_UP);
boolean isValid = deviationRate.compareTo(tolerance) <= 0;
String message = isValid ?
String.format("验证通过,偏差率: %.4f%%", deviationRate.multiply(BigDecimal.valueOf(100))) :
String.format("验证失败,偏差率: %.4f%% 超过容忍度: %.4f%%",
deviationRate.multiply(BigDecimal.valueOf(100)),
tolerance.multiply(BigDecimal.valueOf(100)));
ValidationResult validationResult = ValidationResult.builder()
.isValid(isValid)
.deviation(deviation)
.deviationRate(deviationRate)
.message(message)
.build();
log.debug("验证结果: {}", message);
return validationResult;
}
/**
* 标准化公式表达式
* 处理中文变量名、特殊符号等
*/
private String normalizeFormula(String formula) {
String normalized = formula;
// 替换中文运算符
normalized = normalized.replace("×", "*")
.replace("÷", "/")
.replace("=", "=")
.replace("(", "(")
.replace(")", ")");
// 替换中文变量名为英文
normalized = replaceChineseVariables(normalized);
// 移除空格
normalized = normalized.replaceAll("\\s+", "");
// 标准化等号(如果有)
if (normalized.contains("=")) {
String[] parts = normalized.split("=", 2);
if (parts.length == 2) {
normalized = parts[1]; // 只取等号右边的表达式
}
}
return normalized;
}
/**
* 替换中文变量名为英文
*/
private String replaceChineseVariables(String formula) {
Map<String, String> variableMapping = Map.of(
"资产", "Assets",
"负债", "Liabilities",
"所有者权益", "Equity",
"收入", "Revenue",
"成本", "COGS",
"利润", "Profit",
"流动资产", "CurrentAssets",
"流动负债", "CurrentLiabilities",
"存货", "Inventory",
"净利润", "NetIncome"
);
String result = formula;
for (Map.Entry<String, String> entry : variableMapping.entrySet()) {
result = result.replace(entry.getKey(), entry.getValue());
}
return result;
}
/**
* 验证公式语法
*/
private void validateFormulaSyntax(String formula) throws FormulaCalculationException {
// 使用mXparser验证语法
Expression testExpression = new Expression(formula);
if (!testExpression.checkSyntax()) {
String errorMessage = String.format("公式语法错误: %s, 错误信息: %s",
formula, testExpression.getErrorMessage());
throw new FormulaCalculationException(errorMessage);
}
// 检查是否有未定义的变量
Pattern variablePattern = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");
Matcher matcher = variablePattern.matcher(formula);
while (matcher.find()) {
String variable = matcher.group();
// 跳过数学函数和常量
if (!isMathFunction(variable) && !isMathConstant(variable)) {
log.debug("检测到变量: {}", variable);
}
}
}
/**
* 设置表达式变量
*/
private void setExpressionVariables(Expression expression, Map<String, BigDecimal> variables) {
for (Map.Entry<String, BigDecimal> entry : variables.entrySet()) {
String variableName = entry.getKey();
BigDecimal value = entry.getValue();
if (value != null) {
expression.defineArgument(variableName, value.doubleValue());
log.debug("设置变量 {} = {}", variableName, value);
} else {
log.warn("变量 {} 的值为空,跳过设置", variableName);
}
}
}
/**
* 将财务数据转换为变量映射
*/
private Map<String, BigDecimal> convertFinancialDataToVariables(FinancialData financialData) {
Map<String, BigDecimal> variables = new HashMap<>();
variables.put("CurrentAssets", financialData.getCurrentAssets());
variables.put("CurrentLiabilities", financialData.getCurrentLiabilities());
variables.put("TotalAssets", financialData.getTotalAssets());
variables.put("TotalLiabilities", financialData.getTotalLiabilities());
variables.put("TotalEquity", financialData.getTotalEquity());
variables.put("Revenue", financialData.getRevenue());
variables.put("COGS", financialData.getCogs());
variables.put("NetIncome", financialData.getNetIncome());
variables.put("Inventory", financialData.getInventory());
variables.put("AverageInventory", financialData.getAverageInventory());
return variables;
}
private boolean isMathFunction(String name) {
String[] mathFunctions = {"sin", "cos", "tan", "log", "ln", "exp", "sqrt", "abs"};
for (String func : mathFunctions) {
if (func.equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
private boolean isMathConstant(String name) {
String[] constants = {"pi", "e"};
for (String constant : constants) {
if (constant.equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
}
/**
* 计算结果数据类
*/
@Data
@Builder
class CalculationResult {
private String originalFormula;
private String normalizedFormula;
private BigDecimal calculatedValue;
private Map<String, BigDecimal> variables;
private Boolean success;
private String errorMessage;
private Long calculationTime;
}
/**
* 验证结果数据类
*/
@Data
@Builder
class ValidationResult {
private Boolean isValid;
private BigDecimal deviation;
private BigDecimal deviationRate;
private String message;
}
/**
* 财务数据类
*/
@Data
@Builder
class FinancialData {
private BigDecimal currentAssets;
private BigDecimal currentLiabilities;
private BigDecimal totalAssets;
private BigDecimal totalLiabilities;
private BigDecimal totalEquity;
private BigDecimal revenue;
private BigDecimal cogs;
private BigDecimal netIncome;
private BigDecimal inventory;
private BigDecimal averageInventory;
}
审计分析服务实现
java
package com.company.pdfsystem.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 审计分析服务
* 负责对提取的财务数据进行分析,生成审计发现和建议
*/
@Service
@Slf4j
public class AuditAnalysisService {
private final FormulaCalculationService formulaCalculationService;
private final FinancialDataService financialDataService;
/**
* 生成财务分析报告
*
* @param documentId 文档ID
* @param financialData 财务数据
* @return 财务分析报告
*/
public FinancialAnalysisReport generateFinancialAnalysis(String documentId,
FinancialData financialData) {
log.info("开始生成财务分析报告,文档ID: {}", documentId);
long startTime = System.currentTimeMillis();
FinancialAnalysisReport report = FinancialAnalysisReport.builder()
.documentId(documentId)
.generationTime(System.currentTimeMillis())
.findings(new ArrayList<>())
.recommendations(new ArrayList<>())
.build();
try {
// 1. 计算财务比率
Map<String, CalculationResult> ratios =
formulaCalculationService.calculateFinancialRatios(financialData);
// 2. 分析偿债能力
analyzeLiquidity(report, ratios, financialData);
// 3. 分析盈利能力
analyzeProfitability(report, ratios, financialData);
// 4. 分析运营效率
analyzeEfficiency(report, ratios, financialData);
// 5. 分析财务结构
analyzeFinancialStructure(report, ratios, financialData);
// 6. 生成总体评价
generateOverallAssessment(report);
long processingTime = System.currentTimeMillis() - startTime;
report.setProcessingTime(processingTime);
log.info("财务分析报告生成完成,处理时间: {}ms, 发现数量: {}",
processingTime, report.getFindings().size());
} catch (Exception e) {
log.error("生成财务分析报告失败: {}", e.getMessage(), e);
report.setErrorMessage("生成财务分析报告失败: " + e.getMessage());
}
return report;
}
/**
* 分析偿债能力
*/
private void analyzeLiquidity(FinancialAnalysisReport report,
Map<String, CalculationResult> ratios,
FinancialData financialData) {
log.debug("分析偿债能力");
// 流动比率分析
CalculationResult currentRatio = ratios.get("current_ratio");
if (currentRatio != null && currentRatio.isSuccess()) {
BigDecimal value = currentRatio.getCalculatedValue();
if (value.compareTo(BigDecimal.valueOf(2.0)) < 0) {
AuditFinding finding = AuditFinding.builder()
.category("偿债能力")
.type(FindingType.WARNING)
.title("流动比率偏低")
.description(String.format("流动比率为 %.2f,低于行业标准2.0", value))
.riskLevel(RiskLevel.MEDIUM)
.impact("可能存在短期偿债风险")
.suggestion("建议优化流动资产结构,提高流动性")
.build();
report.getFindings().add(finding);
} else if (value.compareTo(BigDecimal.valueOf(3.0)) > 0) {
AuditFinding finding = AuditFinding.builder()
.category("偿债能力")
.type(FindingType.INFO)
.title("流动比率偏高")
.description(String.format("流动比率为 %.2f,高于3.0", value))
.riskLevel(RiskLevel.LOW)
.impact("资产流动性较强,但可能存在资产利用效率问题")
.suggestion("建议评估流动资产配置效率")
.build();
report.getFindings().add(finding);
}
}
// 速动比率分析
CalculationResult quickRatio = ratios.get("quick_ratio");
if (quickRatio != null && quickRatio.isSuccess()) {
BigDecimal value = quickRatio.getCalculatedValue();
if (value.compareTo(BigDecimal.valueOf(1.0)) < 0) {
AuditFinding finding = AuditFinding.builder()
.category("偿债能力")
.type(FindingType.WARNING)
.title("速动比率偏低")
.description(String.format("速动比率为 %.2f,低于安全水平1.0", value))
.riskLevel(RiskLevel.MEDIUM)
.impact("扣除存货后的短期偿债能力不足")
.suggestion("建议加强应收账款管理,优化存货水平")
.build();
report.getFindings().add(finding);
}
}
}
/**
* 分析盈利能力
*/
private void analyzeProfitability(FinancialAnalysisReport report,
Map<String, CalculationResult> ratios,
FinancialData financialData) {
log.debug("分析盈利能力");
// 净利润率分析
CalculationResult netProfitMargin = ratios.get("net_profit_margin");
if (netProfitMargin != null && netProfitMargin.isSuccess()) {
BigDecimal value = netProfitMargin.getCalculatedValue()
.multiply(BigDecimal.valueOf(100)); // 转换为百分比
if (value.compareTo(BigDecimal.valueOf(5.0)) < 0) {
AuditFinding finding = AuditFinding.builder()
.category("盈利能力")
.type(FindingType.WARNING)
.title("净利润率偏低")
.description(String.format("净利润率为 %.2f%%,低于5%%行业平均水平", value))
.riskLevel(RiskLevel.MEDIUM)
.impact("盈利能力较弱,影响企业可持续发展")
.suggestion("建议分析成本结构,寻找增收节支空间")
.build();
report.getFindings().add(finding);
}
}
// 净资产收益率分析
CalculationResult roe = ratios.get("return_on_equity");
if (roe != null && roe.isSuccess()) {
BigDecimal value = roe.getCalculatedValue()
.multiply(BigDecimal.valueOf(100)); // 转换为百分比
if (value.compareTo(BigDecimal.valueOf(8.0)) < 0) {
AuditFinding finding = AuditFinding.builder()
.category("盈利能力")
.type(FindingType.WARNING)
.title("净资产收益率偏低")
.description(String.format("净资产收益率为 %.2f%%,低于8%%的资本成本", value))
.riskLevel(RiskLevel.HIGH)
.impact("股东投资回报率不足,影响企业价值")
.suggestion("建议优化资本结构,提高资产使用效率")
.build();
report.getFindings().add(finding);
}
}
}
/**
* 分析运营效率
*/
private void analyzeEfficiency(FinancialAnalysisReport report,
Map<String, CalculationResult> ratios,
FinancialData financialData) {
log.debug("分析运营效率");
// 总资产周转率分析
CalculationResult assetTurnover = ratios.get("asset_turnover");
if (assetTurnover != null && assetTurnover.isSuccess()) {
BigDecimal value = assetTurnover.getCalculatedValue();
if (value.compareTo(BigDecimal.valueOf(0.5)) < 0) {
AuditFinding finding = AuditFinding.builder()
.category("运营效率")
.type(FindingType.WARNING)
.title("总资产周转率偏低")
.description(String.format("总资产周转率为 %.2f,资产使用效率不高", value))
.riskLevel(RiskLevel.MEDIUM)
.impact("资产创收能力不足,可能存在闲置资产")
.suggestion("建议评估资产配置,处置低效资产")
.build();
report.getFindings().add(finding);
}
}
// 存货周转率分析
CalculationResult inventoryTurnover = ratios.get("inventory_turnover");
if (inventoryTurnover != null && inventoryTurnover.isSuccess()) {
BigDecimal value = inventoryTurnover.getCalculatedValue();
if (value.compareTo(BigDecimal.valueOf(4.0)) < 0) {
AuditFinding finding = AuditFinding.builder()
.category("运营效率")
.type(FindingType.WARNING)
.title("存货周转率偏低")
.description(String.format("存货周转率为 %.2f,存货流动性不足", value))
.riskLevel(RiskLevel.MEDIUM)
.impact("存货占用资金较多,存在跌价风险")
.suggestion("建议优化存货管理,加强销售预测")
.build();
report.getFindings().add(finding);
}
}
}
/**
* 分析财务结构
*/
private void analyzeFinancialStructure(FinancialAnalysisReport report,
Map<String, CalculationResult> ratios,
FinancialData financialData) {
log.debug("分析财务结构");
// 资产负债率分析
CalculationResult debtToEquity = ratios.get("debt_to_equity");
if (debtToEquity != null && debtToEquity.isSuccess()) {
BigDecimal value = debtToEquity.getCalculatedValue();
if (value.compareTo(BigDecimal.valueOf(2.0)) > 0) {
AuditFinding finding = AuditFinding.builder()
.category("财务结构")
.type(FindingType.ALERT)
.title("资产负债率过高")
.description(String.format("资产负债率为 %.2f,超过2.0的安全水平", value))
.riskLevel(RiskLevel.HIGH)
.impact("财务杠杆过高,存在偿债风险")
.suggestion("建议降低负债水平,优化资本结构")
.build();
report.getFindings().add(finding);
} else if (value.compareTo(BigDecimal.valueOf(0.3)) < 0) {
AuditFinding finding = AuditFinding.builder()
.category("财务结构")
.type(FindingType.INFO)
.title("资产负债率偏低")
.description(String.format("资产负债率为 %.2f,财务政策较为保守", value))
.riskLevel(RiskLevel.LOW)
.impact("财务风险较低,但可能影响发展速度")
.suggestion("建议适度增加财务杠杆,促进业务发展")
.build();
report.getFindings().add(finding);
}
}
}
/**
* 生成总体评价
*/
private void generateOverallAssessment(FinancialAnalysisReport report) {
// 统计各类发现数量
long highRiskCount = report.getFindings().stream()
.filter(f -> f.getRiskLevel() == RiskLevel.HIGH)
.count();
long mediumRiskCount = report.getFindings().stream()
.filter(f -> f.getRiskLevel() == RiskLevel.MEDIUM)
.count();
long lowRiskCount = report.getFindings().stream()
.filter(f -> f.getRiskLevel() == RiskLevel.LOW)
.count();
// 生成总体评价
String overallAssessment;
if (highRiskCount > 0) {
overallAssessment = "存在高风险问题,需要立即关注和改进";
} else if (mediumRiskCount > 2) {
overallAssessment = "存在多个中等风险问题,建议系统性地改进";
} else if (mediumRiskCount > 0) {
overallAssessment = "存在个别中等风险问题,建议针对性改进";
} else {
overallAssessment = "财务状况总体健康,建议继续保持";
}
// 生成改进建议
List<String> overallRecommendations = generateOverallRecommendations(report);
report.setOverallAssessment(overallAssessment);
report.setHighRiskCount((int) highRiskCount);
report.setMediumRiskCount((int) mediumRiskCount);
report.setLowRiskCount((int) lowRiskCount);
report.setRecommendations(overallRecommendations);
log.debug("总体评价生成完成: {}", overallAssessment);
}
/**
* 生成总体改进建议
*/
private List<String> generateOverallRecommendations(FinancialAnalysisReport report) {
List<String> recommendations = new ArrayList<>();
// 根据发现的问题生成建议
if (report.getHighRiskCount() > 0) {
recommendations.add("立即处理高风险问题,制定风险应对预案");
}
if (report.getFindings().stream().anyMatch(f ->
f.getCategory().equals("偿债能力") && f.getRiskLevel() == RiskLevel.MEDIUM)) {
recommendations.add("优化流动资产结构,提高短期偿债能力");
}
if (report.getFindings().stream().anyMatch(f ->
f.getCategory().equals("盈利能力") && f.getRiskLevel() == RiskLevel.MEDIUM)) {
recommendations.add("分析成本费用结构,提升盈利能力");
}
if (report.getFindings().stream().anyMatch(f ->
f.getCategory().equals("财务结构") && f.getRiskLevel() == RiskLevel.HIGH)) {
recommendations.add("优化资本结构,降低财务杠杆");
}
// 默认建议
if (recommendations.isEmpty()) {
recommendations.add("继续保持良好的财务管理实践");
recommendations.add("定期进行财务分析,及时发现问题");
}
return recommendations;
}
/**
* 验证财务报表勾稽关系
*/
public ReconciliationResult verifyFinancialStatement(FinancialData financialData) {
log.info("验证财务报表勾稽关系");
List<ReconciliationFinding> findings = new ArrayList<>();
// 验证资产 = 负债 + 所有者权益
BigDecimal calculatedAssets = financialData.getTotalLiabilities()
.add(financialData.getTotalEquity());
BigDecimal assetDifference = financialData.getTotalAssets()
.subtract(calculatedAssets).abs();
if (assetDifference.compareTo(BigDecimal.valueOf(0.01)) > 0) {
findings.add(ReconciliationFinding.builder()
.item("资产负债表平衡验证")
.expectedValue(calculatedAssets)
.actualValue(financialData.getTotalAssets())
.difference(assetDifference)
.isCritical(true)
.message("资产负债表不平衡,差异: " + assetDifference)
.build());
}
// 验证净利润计算(简化验证)
BigDecimal grossProfit = financialData.getRevenue().subtract(financialData.getCogs());
if (grossProfit.compareTo(financialData.getNetIncome()) < 0) {
findings.add(ReconciliationFinding.builder()
.item("利润表逻辑验证")
.expectedValue(grossProfit)
.actualValue(financialData.getNetIncome())
.difference(grossProfit.subtract(financialData.getNetIncome()).abs())
.isCritical(false)
.message("净利润大于毛利润,可能存在费用分类问题")
.build());
}
ReconciliationResult result = ReconciliationResult.builder()
.findings(findings)
.totalItemsChecked(2)
.criticalFindings((int) findings.stream().filter(ReconciliationFinding::isCritical).count())
.isBalanced(findings.stream().noneMatch(ReconciliationFinding::isCritical))
.build();
log.info("财务报表勾稽关系验证完成,关键问题: {}", result.getCriticalFindings());
return result;
}
}
/**
* 财务分析报告数据类
*/
@Data
@Builder
class FinancialAnalysisReport {
private String documentId;
private Long generationTime;
private Long processingTime;
private String overallAssessment;
private Integer highRiskCount;
private Integer mediumRiskCount;
private Integer lowRiskCount;
private List<AuditFinding> findings;
private List<String> recommendations;
private String errorMessage;
}
/**
* 审计发现数据类
*/
@Data
@Builder
class AuditFinding {
private String category;
private FindingType type;
private String title;
private String description;
private RiskLevel riskLevel;
private String impact;
private String suggestion;
private String reference;
}
/**
* 发现类型枚举
*/
enum FindingType {
INFO, // 信息性发现
WARNING, // 警告性发现
ALERT // 警报性发现
}
/**
* 风险等级枚举
*/
enum RiskLevel {
LOW, // 低风险
MEDIUM, // 中风险
HIGH // 高风险
}
/**
* 勾稽关系验证结果数据类
*/
@Data
@Builder
class ReconciliationResult {
private List<ReconciliationFinding> findings;
private Integer totalItemsChecked;
private Integer criticalFindings;
private Boolean isBalanced;
}
/**
* 勾稽关系发现数据类
*/
@Data
@Builder
class ReconciliationFinding {
private String item;
private BigDecimal expectedValue;
private BigDecimal actualValue;
private BigDecimal difference;
private Boolean isCritical;
private String message;
}
完整的测试用例
java
package com.company.pdfsystem.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* PDF文本提取服务测试类
*/
@ExtendWith(MockitoExtension.class)
class PdfTextExtractionServiceTest {
@Mock
private OcrService ocrService;
@Mock
private TableDetectionService tableDetectionService;
@Mock
private FormulaRecognitionService formulaRecognitionService;
@InjectMocks
private PdfTextExtractionService pdfTextExtractionService;
private MockMultipartFile testPdfFile;
@BeforeEach
void setUp() {
// 创建测试PDF文件
byte[] pdfContent = "Mock PDF content".getBytes();
testPdfFile = new MockMultipartFile(
"file",
"test.pdf",
"application/pdf",
pdfContent
);
}
@Test
@DisplayName("提取PDF文本 - 成功案例")
void testExtractAllText_Success() {
// 准备测试数据
when(tableDetectionService.detectTables(any(), anyInt())).thenReturn(new ArrayList<>());
when(formulaRecognitionService.recognizeFormulas(any(), anyInt())).thenReturn(new ArrayList<>());
// 执行测试
assertDoesNotThrow(() -> {
List<PageTextContent> result = pdfTextExtractionService.extractAllText(testPdfFile);
// 验证结果
assertNotNull(result);
assertTrue(result.size() > 0);
});
}
@Test
@DisplayName("提取PDF表格 - 成功检测表格")
void testExtractTables_Success() {
// 准备测试数据
List<ExtractedTable> mockTables = List.of(
ExtractedTable.builder()
.pageNumber(1)
.rows(List.of(
List.of("资产", "2023", "2022"),
List.of("流动资产", "1000", "800"),
List.of("固定资产", "2000", "1800")
))
.confidence(0.95)
.build()
);
when(tableDetectionService.detectTables(any(), anyInt())).thenReturn(mockTables);
// 执行测试
List<ExtractedTable> result = pdfTextExtractionService.extractTables(testPdfFile);
// 验证结果
assertNotNull(result);
assertEquals(1, result.size());
assertEquals(1, result.get(0).getPageNumber());
assertEquals(3, result.get(0).getRows().size());
assertEquals(0.95, result.get(0).getConfidence());
}
@Test
@DisplayName("处理扫描版PDF - OCR识别成功")
void testProcessScannedPdf_Success() {
// 准备测试数据
OcrPageResult mockPageResult = OcrPageResult.builder()
.pageNumber(1)
.text("扫描文本内容")
.confidence(0.92)
.build();
when(ocrService.recognizeText(any())).thenReturn(mockPageResult);
// 执行测试
OcrResult result = pdfTextExtractionService.processScannedPdf(testPdfFile);
// 验证结果
assertNotNull(result);
assertEquals(1, result.getPageResults().size());
assertEquals("扫描文本内容", result.getPageResults().get(0).getText());
assertEquals(0.92, result.getPageResults().get(0).getConfidence());
}
}
/**
* 公式计算服务测试类
*/
@ExtendWith(MockitoExtension.class)
class FormulaCalculationServiceTest {
@Mock
private FinancialDataService financialDataService;
@InjectMocks
private FormulaCalculationService formulaCalculationService;
@Test
@DisplayName("计算简单公式 - 成功案例")
void testCalculateFormula_Simple_Success() {
// 准备测试数据
String formula = "2 + 3 * 4";
Map<String, BigDecimal> variables = new HashMap<>();
// 执行测试
CalculationResult result = formulaCalculationService.calculateFormula(formula, variables);
// 验证结果
assertNotNull(result);
assertTrue(result.isSuccess());
assertEquals(new BigDecimal("14"), result.getCalculatedValue());
assertEquals("2+3*4", result.getNormalizedFormula());
}
@Test
@DisplayName("计算财务比率 - 流动比率")
void testCalculateFinancialRatios_CurrentRatio() {
// 准备测试数据
FinancialData financialData = FinancialData.builder()
.currentAssets(new BigDecimal("1000"))
.currentLiabilities(new BigDecimal("500"))
.totalAssets(new BigDecimal("2000"))
.totalLiabilities(new BigDecimal("800"))
.totalEquity(new BigDecimal("1200"))
.revenue(new BigDecimal("5000"))
.cogs(new BigDecimal("3000"))
.netIncome(new BigDecimal("500"))
.inventory(new BigDecimal("200"))
.averageInventory(new BigDecimal("180"))
.build();
// 执行测试
Map<String, CalculationResult> results = formulaCalculationService.calculateFinancialRatios(financialData);
// 验证结果
assertNotNull(results);
assertTrue(results.containsKey("current_ratio"));
CalculationResult currentRatio = results.get("current_ratio");
assertTrue(currentRatio.isSuccess());
assertEquals(new BigDecimal("2.0"), currentRatio.getCalculatedValue());
}
@Test
@DisplayName("验证计算结果 - 在容忍范围内")
void testValidateCalculation_WithinTolerance() {
// 准备测试数据
CalculationResult result = CalculationResult.builder()
.calculatedValue(new BigDecimal("100.5"))
.success(true)
.build();
BigDecimal expectedValue = new BigDecimal("100.0");
BigDecimal tolerance = new BigDecimal("0.01"); // 1%容忍度
// 执行测试
ValidationResult validation = formulaCalculationService.validateCalculation(
result, expectedValue, tolerance);
// 验证结果
assertNotNull(validation);
assertTrue(validation.getIsValid());
assertEquals(new BigDecimal("0.5"), validation.getDeviation());
assertEquals(new BigDecimal("0.005"), validation.getDeviationRate());
}
@Test
@DisplayName("验证计算结果 - 超出容忍范围")
void testValidateCalculation_ExceedTolerance() {
// 准备测试数据
CalculationResult result = CalculationResult.builder()
.calculatedValue(new BigDecimal("120.0"))
.success(true)
.build();
BigDecimal expectedValue = new BigDecimal("100.0");
BigDecimal tolerance = new BigDecimal("0.1"); // 10%容忍度
// 执行测试
ValidationResult validation = formulaCalculationService.validateCalculation(
result, expectedValue, tolerance);
// 验证结果
assertNotNull(validation);
assertFalse(validation.getIsValid());
assertEquals(new BigDecimal("20.0"), validation.getDeviation());
assertEquals(new BigDecimal("0.2"), validation.getDeviationRate());
}
}
/**
* 审计分析服务测试类
*/
@ExtendWith(MockitoExtension.class)
class AuditAnalysisServiceTest {
@Mock
private FormulaCalculationService formulaCalculationService;
@Mock
private FinancialDataService financialDataService;
@InjectMocks
private AuditAnalysisService auditAnalysisService;
@Test
@DisplayName("生成财务分析报告 - 发现财务问题")
void testGenerateFinancialAnalysis_WithFindings() {
// 准备测试数据
FinancialData financialData = FinancialData.builder()
.currentAssets(new BigDecimal("800"))
.currentLiabilities(new BigDecimal("600"))
.totalAssets(new BigDecimal("2000"))
.totalLiabilities(new BigDecimal("1500"))
.totalEquity(new BigDecimal("500"))
.revenue(new BigDecimal("3000"))
.cogs(new BigDecimal("2500"))
.netIncome(new BigDecimal("100"))
.inventory(new BigDecimal("300"))
.averageInventory(new BigDecimal("280"))
.build();
// 模拟财务比率计算结果
Map<String, CalculationResult> mockRatios = new HashMap<>();
mockRatios.put("current_ratio", CalculationResult.builder()
.calculatedValue(new BigDecimal("1.33"))
.success(true)
.build());
mockRatios.put("net_profit_margin", CalculationResult.builder()
.calculatedValue(new BigDecimal("0.033"))
.success(true)
.build());
mockRatios.put("debt_to_equity", CalculationResult.builder()
.calculatedValue(new BigDecimal("3.0"))
.success(true)
.build());
when(formulaCalculationService.calculateFinancialRatios(any()))
.thenReturn(mockRatios);
// 执行测试
FinancialAnalysisReport report = auditAnalysisService.generateFinancialAnalysis(
"test-doc-001", financialData);
// 验证结果
assertNotNull(report);
assertEquals("test-doc-001", report.getDocumentId());
assertFalse(report.getFindings().isEmpty());
// 验证发现了资产负债率过高的问题
boolean hasHighDebtFinding = report.getFindings().stream()
.anyMatch(f -> f.getTitle().contains("资产负债率过高"));
assertTrue(hasHighDebtFinding);
// 验证总体评价
assertNotNull(report.getOverallAssessment());
assertNotNull(report.getRecommendations());
}
@Test
@DisplayName("验证财务报表勾稽关系 - 平衡案例")
void testVerifyFinancialStatement_Balanced() {
// 准备测试数据(平衡的财务报表)
FinancialData balancedData = FinancialData.builder()
.totalAssets(new BigDecimal("1000"))
.totalLiabilities(new BigDecimal("600"))
.totalEquity(new BigDecimal("400"))
.revenue(new BigDecimal("500"))
.cogs(new BigDecimal("300"))
.netIncome(new BigDecimal("150")) // 假设其他费用50
.build();
// 执行测试
ReconciliationResult result = auditAnalysisService.verifyFinancialStatement(balancedData);
// 验证结果
assertNotNull(result);
assertTrue(result.getIsBalanced());
assertEquals(0, result.getCriticalFindings());
}
@Test
@DisplayName("验证财务报表勾稽关系 - 不平衡案例")
void testVerifyFinancialStatement_Unbalanced() {
// 准备测试数据(不平衡的财务报表)
FinancialData unbalancedData = FinancialData.builder()
.totalAssets(new BigDecimal("1000"))
.totalLiabilities(new BigDecimal("600"))
.totalEquity(new BigDecimal("300")) // 应该是400,不平衡
.build();
// 执行测试
ReconciliationResult result = auditAnalysisService.verifyFinancialStatement(unbalancedData);
// 验证结果
assertNotNull(result);
assertFalse(result.getIsBalanced());
assertTrue(result.getCriticalFindings() > 0);
}
}
/**
* 集成测试 - 完整业务流程
*/
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PdfIntelligenceIntegrationTest {
@Autowired
private PdfTextExtractionService pdfTextExtractionService;
@Autowired
private FormulaCalculationService formulaCalculationService;
@Autowired
private AuditAnalysisService auditAnalysisService;
@Autowired
private PdfDocumentRepository pdfDocumentRepository;
private PdfDocument testDocument;
@BeforeAll
void setUp() {
// 创建测试文档
testDocument = PdfDocument.builder()
.documentId("TEST-DOC-001")
.fileName("test_financial_statement.pdf")
.originalFileName("financial_statement_2023.pdf")
.filePath("/test/path/document.pdf")
.fileSize(1024000L)
.fileHash("test123456789")
.documentType("FINANCIAL_STATEMENT")
.clientName("测试公司")
.fiscalYear("2023")
.status(PdfDocument.DocumentStatus.PARSED)
.totalPages(10)
.parseProgress(100)
.uploadedBy("test-user")
.uploadTime(LocalDateTime.now())
.parseCompletedTime(LocalDateTime.now())
.build();
pdfDocumentRepository.save(testDocument);
}
@Test
@DisplayName("完整业务流程 - 从PDF解析到审计报告")
void testCompleteWorkflow() {
// 1. 准备测试财务数据
FinancialData financialData = FinancialData.builder()
.currentAssets(new BigDecimal("1500000"))
.currentLiabilities(new BigDecimal("750000"))
.totalAssets(new BigDecimal("5000000"))
.totalLiabilities(new BigDecimal("2000000"))
.totalEquity(new BigDecimal("3000000"))
.revenue(new BigDecimal("8000000"))
.cogs(new BigDecimal("4800000"))
.netIncome(new BigDecimal("800000"))
.inventory(new BigDecimal("300000"))
.averageInventory(new BigDecimal("280000"))
.build();
// 2. 生成财务分析报告
FinancialAnalysisReport analysisReport = auditAnalysisService.generateFinancialAnalysis(
testDocument.getDocumentId(), financialData);
// 3. 验证分析报告
assertNotNull(analysisReport);
assertTrue(analysisReport.getProcessingTime() > 0);
assertFalse(analysisReport.getFindings().isEmpty());
assertNotNull(analysisReport.getOverallAssessment());
// 4. 验证勾稽关系
ReconciliationResult reconciliation = auditAnalysisService.verifyFinancialStatement(financialData);
assertNotNull(reconciliation);
// 5. 记录测试结果
log.info("集成测试完成 - 发现数量: {}, 高风险: {}, 处理时间: {}ms",
analysisReport.getFindings().size(),
analysisReport.getHighRiskCount(),
analysisReport.getProcessingTime());
}
@Test
@DisplayName("性能测试 - 大批量公式计算")
void testPerformance_BatchFormulaCalculation() {
// 准备测试数据
int batchSize = 100;
List<Map<String, BigDecimal>> testCases = new ArrayList<>();
for (int i = 0; i < batchSize; i++) {
Map<String, BigDecimal> variables = new HashMap<>();
variables.put("A", new BigDecimal(i * 1000));
variables.put("B", new BigDecimal(i * 100));
variables.put("C", new BigDecimal(i * 10));
testCases.add(variables);
}
String formula = "A + B * C - (A / B)";
// 执行性能测试
long startTime = System.currentTimeMillis();
List<CalculationResult> results = testCases.stream()
.map(variables -> {
try {
return formulaCalculationService.calculateFormula(formula, variables);
} catch (Exception e) {
return CalculationResult.builder()
.success(false)
.errorMessage(e.getMessage())
.build();
}
})
.collect(Collectors.toList());
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
// 验证性能
long successCount = results.stream().filter(CalculationResult::isSuccess).count();
double successRate = (double) successCount / batchSize * 100;
double averageTime = (double) totalTime / batchSize;
log.info("性能测试结果 - 总数: {}, 成功: {} ({}%), 总时间: {}ms, 平均时间: {:.2f}ms",
batchSize, successCount, successRate, totalTime, averageTime);
// 性能断言
assertTrue(successRate >= 95, "成功率应大于95%");
assertTrue(averageTime < 10, "平均计算时间应小于10ms");
}
}