dingdanquanliucheng/backend/app/services/export_service.py

255 lines
10 KiB
Python
Raw Normal View History

"""
业绩报表导出服务模块
职责
将业绩统计数据导出为 CSV Excelxlsx格式的文件供用户下载
负责文件命名格式校验内容写入和格式美化
依赖
- openpyxlExcel 文件生成
- csv标准库 CSV 写入
被引用方
- ReportService.export_performance()调用本服务生成导出文件
"""
import csv
from datetime import datetime
from pathlib import Path
from openpyxl import Workbook
class ExportService:
"""业绩报表导出服务。
根据传入的业绩报表数据生成 CSV Excel 格式的导出文件
并返回文件元信息路径大小MIME 类型等供上层服务上传到 OSS
2026-06-14 16:20:04 +08:00
导出文件存储在本地 /tmp/order-flow-exports 目录下
"""
def __init__(self) -> None:
2026-06-14 16:20:04 +08:00
from backend.app.core.config import get_settings
settings = get_settings()
# 复用 LOCAL_UPLOAD_DIR 的父目录,保持路径风格一致
self.export_root = Path(settings.local_upload_dir).parent / "exports"
self.export_root.mkdir(parents=True, exist_ok=True)
def build_performance_export(self, report: dict, export_format: str = "csv", raw_rows: list | None = None) -> dict:
"""根据业绩报表数据生成导出文件(含订单明细)。
根据指定的导出格式csv xlsx将汇总数据和订单明细写入本地文件
并返回文件元信息供上层服务上传到 OSS
参数
report (dict): 业绩报表数据包含 stat_type统计类型 list数据列表
export_format (str): 导出格式支持 "csv" "xlsx"默认 "csv"
raw_rows (list): 订单明细行列表用于在导出中追加明细数据
返回
dict: 包含 file_namefile_pathfile_sizecontent_type
object_keyexport_format 等字段
被调用方
- ReportService.export_performance() 业绩报表导出 API
"""
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
suffix = report["stat_type"]
normalized_format = self._normalize_export_format(export_format)
file_name = f"performance-{suffix}-{timestamp}.{normalized_format}"
file_path = self.export_root / file_name
if normalized_format == "xlsx":
self._write_xlsx(file_path, report, raw_rows)
content_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
else:
self._write_csv(file_path, report, raw_rows)
content_type = "text/csv"
return {
"file_name": file_name,
"file_path": str(file_path),
"file_size": file_path.stat().st_size,
"content_type": content_type,
"object_key": f"exports/performance/{file_name}",
"export_format": normalized_format,
}
def _normalize_export_format(self, export_format: str) -> str:
"""校验并标准化导出格式。
将传入的格式字符串转为小写并去除空白若不在支持的格式列表中
则默认回退为 csv
参数
export_format (str): 用户指定的导出格式
返回
str: 标准化后的格式字符串"csv" "xlsx"
"""
normalized = (export_format or "csv").strip().lower()
if normalized not in {"csv", "xlsx"}:
return "csv"
return normalized
def _format_stat_period(self, dt, stat_type: str) -> str:
"""将日期按统计类型格式化为统计周期标识字符串。"""
if dt is None:
return ""
if stat_type == "year":
return dt.strftime("%Y")
if stat_type == "quarter":
quarter = ((dt.month - 1) // 3) + 1
return f"{dt.year}-Q{quarter}"
return dt.strftime("%Y-%m")
def _build_detail_map(self, raw_rows: list | None, stat_type: str = "month") -> dict[str, list]:
"""将订单明细行按统计周期分组。"""
if not raw_rows:
return {}
detail_map: dict[str, list] = {}
for row in raw_rows:
if row.created_at is None:
continue
period = self._format_stat_period(row.created_at, stat_type)
detail_map.setdefault(period, []).append(row)
return detail_map
def _write_csv(self, file_path: Path, report: dict, raw_rows: list | None = None) -> None:
"""将报表汇总和订单明细写入 CSV 文件。
使用 UTF-8-BOM 编码写入确保 Excel 打开时中文不乱码
第一行为汇总表头每个周期后追加该周期的订单明细行
参数
file_path (Path): 目标 CSV 文件路径
report (dict): 报表数据包含 list 列表
raw_rows (list): 订单明细行列表用于追加明细数据
"""
detail_map = self._build_detail_map(raw_rows, report["stat_type"])
with file_path.open("w", encoding="utf-8-sig", newline="") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["统计周期", "订单数", "订单金额", "固定提成", "分类明细"])
for item in report["list"]:
writer.writerow(self._build_row(item))
for detail in detail_map.get(item["stat_period"], []):
writer.writerow([
"",
detail.order_no or "",
detail.customer_name or "",
detail.product_name or "",
detail.quantity or "",
f"{float(detail.sale_price or 0):.2f}",
f"{float(detail.contract_amount or 0):.2f}",
f"{float(detail.profit_total or 0):.2f}",
f"{float(detail.commission_amount or 0):.2f}",
(detail.created_at.strftime("%Y-%m-%d") if detail.created_at else ""),
])
writer.writerow(self._build_summary_row(report["list"]))
def _write_xlsx(self, file_path: Path, report: dict, raw_rows: list | None = None) -> None:
"""将报表汇总和订单明细写入 Excel 文件。
使用 openpyxl 创建工作簿第一个 sheet 为汇总数据
第二个 sheet 为订单明细
参数
file_path (Path): 目标 xlsx 文件路径
report (dict): 报表数据包含 list 列表
raw_rows (list): 订单明细行列表
"""
workbook = Workbook()
# Sheet 1: 汇总
sheet = workbook.active
sheet.title = "业绩统计"
sheet.append(["统计周期", "订单数", "订单金额", "固定提成", "分类明细"])
for item in report["list"]:
sheet.append(self._build_row(item))
summary = self._build_summary_row(report["list"])
sheet.append(summary)
for cell in sheet[1]:
cell.font = cell.font.copy(bold=True)
last_row = sheet.max_row
for cell in sheet[last_row]:
cell.font = cell.font.copy(bold=True)
sheet.column_dimensions["A"].width = 18
sheet.column_dimensions["B"].width = 12
sheet.column_dimensions["C"].width = 16
sheet.column_dimensions["D"].width = 16
sheet.column_dimensions["E"].width = 48
# Sheet 2: 订单明细
if raw_rows:
detail_sheet = workbook.create_sheet("订单明细")
detail_sheet.append(["统计周期", "订单号", "客户", "产品", "数量", "单价", "金额", "利润", "提成", "日期"])
for row in raw_rows:
stat_period = self._format_stat_period(row.created_at, report["stat_type"])
detail_sheet.append([
stat_period,
row.order_no or "",
row.customer_name or "",
row.product_name or "",
row.quantity or "",
f"{float(row.sale_price or 0):.2f}",
f"{float(row.contract_amount or 0):.2f}",
f"{float(row.profit_total or 0):.2f}",
f"{float(row.commission_amount or 0):.2f}",
row.created_at.strftime("%Y-%m-%d") if row.created_at else "",
])
for cell in detail_sheet[1]:
cell.font = cell.font.copy(bold=True)
detail_sheet.column_dimensions["A"].width = 18
detail_sheet.column_dimensions["B"].width = 22
detail_sheet.column_dimensions["C"].width = 18
detail_sheet.column_dimensions["D"].width = 24
detail_sheet.column_dimensions["E"].width = 8
detail_sheet.column_dimensions["F"].width = 10
detail_sheet.column_dimensions["G"].width = 12
detail_sheet.column_dimensions["H"].width = 12
detail_sheet.column_dimensions["I"].width = 10
detail_sheet.column_dimensions["J"].width = 14
workbook.save(file_path)
def _build_summary_row(self, items: list) -> list:
"""构建合计行。"""
total_count = sum(item["order_count"] for item in items)
total_amount = sum(item["order_amount"] for item in items)
total_commission = sum(item["commission_amount"] for item in items)
return [
"合计",
total_count,
f"{total_amount:.2f}",
f"{total_commission:.2f}",
"",
]
def _build_row(self, item: dict) -> list:
"""将单条报表数据项转换为表格行。
将分类明细的金额列表用中文分号连接为可读字符串
数值字段格式化为保留两位小数的字符串
参数
item (dict): 单条报表数据包含 stat_periodorder_count
order_amountcommission_amountcategory_amounts
返回
list: 包含 [统计周期, 订单数, 订单金额, 固定提成, 分类明细] 的列表
"""
category_text = "".join(
f"{category['category_name']}:{category['amount']:.2f}"
for category in item.get("category_amounts", [])
)
return [
item["stat_period"],
item["order_count"],
f"{item['order_amount']:.2f}",
f"{item['commission_amount']:.2f}",
category_text,
]
export_service = ExportService()