dingdanquanliucheng/backend/app/repositories/report_repository.py
2026-06-14 16:20:04 +08:00

118 lines
4.8 KiB
Python
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.

"""报表模块 Repository 层
负责销售绩效报表、业绩统计等报表相关数据的复杂查询操作。
被 ReportService报表服务调用。
"""
from datetime import datetime, timedelta
from sqlalchemy import and_, func, not_, or_, select
from sqlalchemy.orm import Session
from backend.app.models.business import Product, ProductCategory, SalesOrder, SalesOrderItem
# 电商来源关键词,用于排除电商渠道订单
ECOMMERCE_SOURCE_KEYWORDS = ["电商", "淘宝", "天猫", "京东", "拼多多", "1688", "线上", "抖音", "快手"]
class ReportRepository:
"""报表数据仓储
封装销售绩效等报表的复杂多表联查操作。
被 ReportService报表服务调用。
"""
def list_performance_rows(self, session: Session, filters: dict) -> list:
"""查询销售绩效明细数据
联表查询销售订单、订单明细、产品和产品分类,
返回按订单和明细粒度的绩效数据行。
支持按日期范围、产品分类过滤,以及排除电商渠道订单。
参数:
session: 数据库会话
filters: 过滤条件字典,支持 start_date / end_date / category_id / exclude_ecommerce
返回:
包含 order_id / created_at / order_source / commission_amount /
quantity / sale_price / category_id / category_name 的行列表
"""
stmt = (
select(
SalesOrder.id.label("order_id"),
SalesOrder.created_at.label("created_at"),
SalesOrder.order_source.label("order_source"),
SalesOrder.commission_amount.label("commission_amount"),
SalesOrder.sale_price_total.label("sale_price_total"),
SalesOrder.cost_price_total.label("cost_price_total"),
SalesOrder.profit_total.label("profit_total"),
SalesOrder.profit_rate.label("profit_rate"),
SalesOrderItem.quantity.label("quantity"),
SalesOrderItem.sale_price.label("sale_price"),
Product.category_id.label("category_id"),
ProductCategory.category_name.label("category_name"),
)
.join(
SalesOrderItem,
and_(SalesOrderItem.order_id == SalesOrder.id, SalesOrderItem.deleted == 0),
)
.outerjoin(
Product,
and_(Product.id == SalesOrderItem.product_id, Product.deleted == 0),
)
.outerjoin(
ProductCategory,
and_(ProductCategory.id == Product.category_id, ProductCategory.deleted == 0),
)
.where(
SalesOrder.deleted == 0,
SalesOrder.order_status != "canceled",
)
)
if filters.get("start_date"):
stmt = stmt.where(SalesOrder.created_at >= filters["start_date"])
if filters.get("end_date"):
stmt = stmt.where(SalesOrder.created_at < filters["end_date"] + timedelta(days=1))
if filters.get("category_id") is not None:
stmt = stmt.where(Product.category_id == filters["category_id"])
if filters.get("exclude_ecommerce"):
ecommerce_conditions = [
SalesOrder.order_source.like(f"%{kw}%") for kw in ECOMMERCE_SOURCE_KEYWORDS
]
stmt = stmt.where(not_(or_(*ecommerce_conditions)))
if filters.get("salesman_id") is not None:
stmt = stmt.where(SalesOrder.salesman_id == filters["salesman_id"])
stmt = stmt.order_by(SalesOrder.created_at.asc(), SalesOrder.id.asc(), SalesOrderItem.id.asc())
return list(session.execute(stmt).all())
def count_excluded_orders(self, session: Session, filters: dict) -> int:
"""统计被电商排除条件筛掉的订单数量
参数:
session: 数据库会话
filters: 过滤条件字典,需包含 start_date / end_date
返回:
被排除的订单数量
"""
stmt = (
select(func.count(func.distinct(SalesOrder.id)))
.where(
SalesOrder.deleted == 0,
SalesOrder.order_status != "canceled",
)
)
if filters.get("start_date"):
stmt = stmt.where(SalesOrder.created_at >= filters["start_date"])
if filters.get("end_date"):
stmt = stmt.where(SalesOrder.created_at < filters["end_date"] + timedelta(days=1))
if filters.get("salesman_id") is not None:
stmt = stmt.where(SalesOrder.salesman_id == filters["salesman_id"])
ecommerce_conditions = [
SalesOrder.order_source.like(f"%{kw}%") for kw in ECOMMERCE_SOURCE_KEYWORDS
]
stmt = stmt.where(or_(*ecommerce_conditions))
return session.execute(stmt).scalar() or 0