Files
shanxia2.0/backend/app/crud/statistics.py
T
2026-05-12 16:35:07 +08:00

917 lines
33 KiB
Python

"""数据统计 CRUD"""
from sqlalchemy.orm import Session
from sqlalchemy import and_, func, desc, extract
from typing import Optional, List, Dict, Any, Tuple
from datetime import datetime, timedelta
from decimal import Decimal
from app.models.models import (
BizOrder, BizOrderItem, BizDeliveryTask,
BizSettlement, BizRefund, BizPriceList, BizPriceListItem,
SysUser, SysRegion, BasProduct,
BizInquiry, BizQuotation, BizDeliveryWarning,
)
class StatisticsCRUD:
"""数据统计CRUD操作"""
@staticmethod
def get_order_statistics(
db: Session,
region_id: str = None,
start_date: str = None,
end_date: str = None
) -> Dict[str, Any]:
"""获取订单统计数据"""
query = db.query(BizOrder).filter(BizOrder.is_del == 0)
if region_id:
query = query.filter(BizOrder.region_id == region_id)
if start_date:
try:
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
query = query.filter(BizOrder.create_at >= start_dt)
except:
pass
if end_date:
try:
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
end_dt = end_dt.replace(hour=23, minute=59, second=59)
query = query.filter(BizOrder.create_at <= end_dt)
except:
pass
orders = query.all()
total_orders = len(orders)
pending_payment = sum(1 for o in orders if o.status == "pending_payment")
pending_dispatch = sum(1 for o in orders if getattr(o, "delivery_status", None) == "pending_dispatch")
delivering = sum(1 for o in orders if o.status == "delivering")
completed = sum(1 for o in orders if o.status == "completed")
cancelled = sum(1 for o in orders if o.status == "cancelled")
total_amount = sum(float(o.total_amount or 0) for o in orders)
paid_amount = sum(float(o.paid_amount or 0) for o in orders)
refund_amount = sum(float(o.refund_amount or 0) for o in orders)
return {
"totalOrders": total_orders,
"pendingPayment": pending_payment,
"pendingDelivery": pending_dispatch,
"delivering": delivering,
"completed": completed,
"cancelled": cancelled,
"totalAmount": round(total_amount, 2),
"paidAmount": round(paid_amount, 2),
"refundAmount": round(refund_amount, 2)
}
@staticmethod
def get_order_trend(
db: Session,
region_id: str = None,
days: int = 7,
include_inactive_regions: bool = False,
) -> List[Dict[str, Any]]:
"""获取订单趋势数据(按日统计)"""
result = []
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
for i in range(days - 1, -1, -1):
day_start = today - timedelta(days=i)
day_end = day_start + timedelta(days=1)
query = (
db.query(BizOrder)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(
and_(
BizOrder.is_del == 0,
BizOrder.create_at >= day_start,
BizOrder.create_at < day_end,
SysRegion.is_del == 0,
*region_active_clause,
)
)
)
if region_id:
query = query.filter(BizOrder.region_id == region_id)
orders = query.all()
order_count = len(orders)
order_amount = sum(float(o.total_amount or 0) for o in orders)
result.append({
"date": day_start.strftime("%Y-%m-%d"),
"orderCount": order_count,
"orderAmount": round(order_amount, 2)
})
return result
@staticmethod
def get_today_order_stats_by_region(
db: Session,
region_id: str = None,
include_inactive_regions: bool = False,
) -> List[Dict[str, Any]]:
"""今日各区域营业额(元)、订单笔数;列出未删除的区域,无单则为 0。"""
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
day_end = today + timedelta(days=1)
region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
reg_q = db.query(SysRegion).filter(
SysRegion.is_del == 0,
)
if region_id:
reg_q = reg_q.filter(SysRegion.id == region_id)
regions = reg_q.order_by(SysRegion.sort_order, SysRegion.name).all()
agg_q = (
db.query(
BizOrder.region_id,
func.count(BizOrder.id).label("order_count"),
func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"),
)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(
BizOrder.is_del == 0,
BizOrder.create_at >= today,
BizOrder.create_at < day_end,
SysRegion.is_del == 0,
*region_active_clause,
)
.group_by(BizOrder.region_id)
)
if region_id:
agg_q = agg_q.filter(BizOrder.region_id == region_id)
agg_map: Dict[str, Any] = {r.region_id: r for r in agg_q.all() if r.region_id}
out: List[Dict[str, Any]] = []
for reg in regions:
row = agg_map.get(reg.id)
cnt = int(row.order_count or 0) if row else 0
amt = float(row.order_amount or 0) if row else 0.0
out.append({
"regionId": reg.id,
"regionName": reg.name or reg.id,
"orderCount": cnt,
"orderAmount": round(amt, 2),
})
return out
@staticmethod
def get_today_order_stats_by_client(
db: Session,
region_id: str = None,
limit: int = 30,
include_inactive_regions: bool = False,
) -> List[Dict[str, Any]]:
"""今日各下单客户:下单金额合计(元)、商品数量合计(订单行数量之和);按金额降序,最多 limit 条。"""
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
day_end = today + timedelta(days=1)
region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
amt_q = (
db.query(
BizOrder.client_id,
func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"),
)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(
BizOrder.is_del == 0,
BizOrder.create_at >= today,
BizOrder.create_at < day_end,
SysRegion.is_del == 0,
*region_active_clause,
)
)
if region_id:
amt_q = amt_q.filter(BizOrder.region_id == region_id)
amt_rows = amt_q.group_by(BizOrder.client_id).all()
qty_q = (
db.query(
BizOrder.client_id,
func.coalesce(func.sum(BizOrderItem.quantity), 0).label("qty"),
)
.select_from(BizOrderItem)
.join(
BizOrder,
and_(
BizOrder.id == BizOrderItem.order_id,
BizOrder.is_del == 0,
BizOrder.create_at >= today,
BizOrder.create_at < day_end,
),
)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(BizOrderItem.is_del == 0)
.filter(SysRegion.is_del == 0, *region_active_clause)
)
if region_id:
qty_q = qty_q.filter(BizOrder.region_id == region_id)
qty_rows = qty_q.group_by(BizOrder.client_id).all()
qty_map: Dict[str, float] = {
r.client_id: float(r.qty or 0) for r in qty_rows if r.client_id
}
merged: List[Dict[str, Any]] = []
for r in amt_rows:
cid = r.client_id
if not cid:
continue
merged.append({
"clientId": cid,
"orderAmount": round(float(r.order_amount or 0), 2),
"productQuantity": round(float(qty_map.get(cid, 0) or 0), 4),
})
merged.sort(key=lambda x: -x["orderAmount"])
merged = merged[: max(1, min(int(limit or 30), 100))]
client_ids = [m["clientId"] for m in merged]
names: Dict[str, str] = {}
if client_ids:
for u in (
db.query(SysUser)
.filter(and_(SysUser.is_del == 0, SysUser.id.in_(client_ids)))
.all()
):
names[u.id] = (u.real_name or u.phone or u.username or u.id) or u.id
out: List[Dict[str, Any]] = []
for m in merged:
cid = m["clientId"]
nm = names.get(cid, cid)
out.append({
"clientId": cid,
"clientName": nm,
"orderAmount": m["orderAmount"],
"productQuantity": m["productQuantity"],
})
return out
@staticmethod
def get_financial_statistics(
db: Session,
region_id: str = None,
start_date: str = None,
end_date: str = None
) -> Dict[str, Any]:
"""获取财务统计数据"""
order_query = db.query(BizOrder).filter(BizOrder.is_del == 0)
settlement_query = db.query(BizSettlement).filter(BizSettlement.is_del == 0)
refund_query = db.query(BizRefund).filter(BizRefund.is_del == 0)
if region_id:
order_query = order_query.filter(BizOrder.region_id == region_id)
settlement_query = settlement_query.filter(BizSettlement.region_id == region_id)
if start_date:
try:
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
order_query = order_query.filter(BizOrder.create_at >= start_dt)
settlement_query = settlement_query.filter(BizSettlement.create_at >= start_dt)
refund_query = refund_query.filter(BizRefund.create_at >= start_dt)
except:
pass
if end_date:
try:
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
end_dt = end_dt.replace(hour=23, minute=59, second=59)
order_query = order_query.filter(BizOrder.create_at <= end_dt)
settlement_query = settlement_query.filter(BizSettlement.create_at <= end_dt)
refund_query = refund_query.filter(BizRefund.create_at <= end_dt)
except:
pass
orders = order_query.all()
settlements = settlement_query.filter(BizSettlement.status == "confirmed").all()
refunds = refund_query.filter(BizRefund.status == "completed").all()
total_revenue = sum(float(o.total_amount or 0) for o in orders)
total_received = sum(float(s.amount or 0) for s in settlements)
total_refund = sum(float(r.refund_amount or 0) for r in refunds)
net_revenue = total_received - total_refund
return {
"totalRevenue": round(total_revenue, 2),
"totalReceived": round(total_received, 2),
"totalRefund": round(total_refund, 2),
"netRevenue": round(net_revenue, 2),
"orderCount": len(orders),
"settlementCount": len(settlements),
"refundCount": len(refunds)
}
@staticmethod
def get_financial_trend(
db: Session,
region_id: str = None,
days: int = 7
) -> List[Dict[str, Any]]:
"""获取财务趋势数据(按日统计)"""
result = []
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
for i in range(days - 1, -1, -1):
day_start = today - timedelta(days=i)
day_end = day_start + timedelta(days=1)
settlement_query = db.query(BizSettlement).filter(
and_(
BizSettlement.is_del == 0,
BizSettlement.status == "confirmed",
BizSettlement.create_at >= day_start,
BizSettlement.create_at < day_end
)
)
refund_query = db.query(BizRefund).filter(
and_(
BizRefund.is_del == 0,
BizRefund.status == "completed",
BizRefund.create_at >= day_start,
BizRefund.create_at < day_end
)
)
if region_id:
settlement_query = settlement_query.filter(BizSettlement.region_id == region_id)
settlements = settlement_query.all()
refunds = refund_query.all()
received = sum(float(s.amount or 0) for s in settlements)
refund = sum(float(r.refund_amount or 0) for r in refunds)
result.append({
"date": day_start.strftime("%Y-%m-%d"),
"received": round(received, 2),
"refund": round(refund, 2),
"net": round(received - refund, 2)
})
return result
@staticmethod
def get_delivery_statistics(
db: Session,
region_id: str = None,
start_date: str = None,
end_date: str = None
) -> Dict[str, Any]:
"""获取配送统计数据"""
query = db.query(BizDeliveryTask).filter(BizDeliveryTask.is_del == 0)
if region_id:
query = query.filter(BizDeliveryTask.region_id == region_id)
if start_date:
try:
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
query = query.filter(BizDeliveryTask.create_at >= start_dt)
except:
pass
if end_date:
try:
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
end_dt = end_dt.replace(hour=23, minute=59, second=59)
query = query.filter(BizDeliveryTask.create_at <= end_dt)
except:
pass
tasks = query.all()
pending = sum(1 for t in tasks if t.status == "pending")
picked_up = sum(1 for t in tasks if t.status == "picked_up")
delivering = sum(1 for t in tasks if t.status == "delivering")
completed = sum(1 for t in tasks if t.status == "completed")
cancelled = sum(1 for t in tasks if t.status == "cancelled")
return {
"totalTasks": len(tasks),
"pending": pending,
"pickedUp": picked_up,
"delivering": delivering,
"completed": completed,
"cancelled": cancelled
}
@staticmethod
def get_product_statistics(
db: Session,
region_id: str = None,
start_date: str = None,
end_date: str = None,
limit: int = 10
) -> List[Dict[str, Any]]:
"""获取商品销售排行"""
query = db.query(
BizOrderItem.product_id,
func.sum(BizOrderItem.quantity).label("total_quantity"),
func.sum(BizOrderItem.amount).label("total_amount")
).join(
BizOrder,
and_(
BizOrder.id == BizOrderItem.order_id,
BizOrder.is_del == 0,
BizOrder.status.in_(["completed", "delivering", "pending_confirm"])
)
).filter(
BizOrderItem.is_del == 0
)
if region_id:
query = query.filter(BizOrder.region_id == region_id)
if start_date:
try:
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
query = query.filter(BizOrder.create_at >= start_dt)
except:
pass
if end_date:
try:
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
end_dt = end_dt.replace(hour=23, minute=59, second=59)
query = query.filter(BizOrder.create_at <= end_dt)
except:
pass
results = query.group_by(BizOrderItem.product_id).order_by(
desc("total_amount")
).limit(limit).all()
product_stats = []
for r in results:
product = db.query(BasProduct).filter(
and_(BasProduct.id == r.product_id, BasProduct.is_del == 0)
).first()
product_stats.append({
"productId": r.product_id,
"productName": product.name if product else None,
"totalQuantity": float(r.total_quantity or 0),
"totalAmount": float(r.total_amount or 0)
})
return product_stats
@staticmethod
def get_customer_statistics(
db: Session,
region_id: str = None,
start_date: str = None,
end_date: str = None,
limit: int = 10
) -> List[Dict[str, Any]]:
"""获取客户订单排行"""
query = db.query(
BizOrder.client_id,
func.count(BizOrder.id).label("order_count"),
func.sum(BizOrder.total_amount).label("total_amount")
).filter(
and_(
BizOrder.is_del == 0,
BizOrder.status.in_(["completed", "delivering", "pending_confirm", "pending_payment"])
)
)
if region_id:
query = query.filter(BizOrder.region_id == region_id)
if start_date:
try:
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
query = query.filter(BizOrder.create_at >= start_dt)
except:
pass
if end_date:
try:
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
end_dt = end_dt.replace(hour=23, minute=59, second=59)
query = query.filter(BizOrder.create_at <= end_dt)
except:
pass
results = query.group_by(BizOrder.client_id).order_by(
desc("total_amount")
).limit(limit).all()
customer_stats = []
for r in results:
customer = db.query(SysUser).filter(
and_(SysUser.id == r.client_id, SysUser.is_del == 0)
).first()
customer_stats.append({
"customerId": r.client_id,
"customerName": customer.real_name if customer else None,
"customerPhone": customer.phone if customer else None,
"orderCount": r.order_count,
"totalAmount": float(r.total_amount or 0)
})
return customer_stats
@staticmethod
def _dashboard_order_region_clause(region_id: str = None):
return [BizOrder.region_id == region_id] if region_id else []
@staticmethod
def get_dashboard_summary(
db: Session,
region_id: str = None,
include_inactive_regions: bool = False,
) -> Dict[str, Any]:
"""获取仪表盘汇总数据(运营工作台;含待办摘要、最新订单、预警数等,字段驼峰)"""
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
tomorrow_start = today + timedelta(days=1)
month_start = today.replace(day=1)
yesterday_start = today - timedelta(days=1)
region_clause = StatisticsCRUD._dashboard_order_region_clause(region_id)
region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
order_query = (
db.query(BizOrder)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(
and_(
BizOrder.is_del == 0,
BizOrder.create_at >= month_start,
SysRegion.is_del == 0,
*region_active_clause,
*region_clause,
)
)
)
month_orders = order_query.all()
today_orders = [
o for o in month_orders if o.create_at and o.create_at >= today
]
prev_day_orders = [
o
for o in month_orders
if o.create_at and yesterday_start <= o.create_at < today
]
pending_delivery_q = (
db.query(BizOrder)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(
and_(
BizOrder.is_del == 0,
BizOrder.delivery_status == "pending_dispatch",
SysRegion.is_del == 0,
*region_active_clause,
*region_clause,
)
)
)
pending_delivery_count = pending_delivery_q.count()
def _delta_hint(cur_n: int, prev_n: int, unit: str) -> str:
d = cur_n - prev_n
if d > 0:
return f"较昨日 +{d}{unit}"
if d < 0:
return f"较昨日 {d}{unit}"
return "与昨日持平"
def _amount_delta_hint(cur_amt: float, prev_amt: float) -> str:
d = round(cur_amt - prev_amt, 2)
if d > 0.0001:
return f"较昨日 +¥{d:,.2f}"
if d < -0.0001:
return f"较昨日 ¥{d:,.2f}"
return "与昨日持平"
today_order_count = len(today_orders)
today_order_amount = round(sum(float(o.total_amount or 0) for o in today_orders), 2)
prev_count = len(prev_day_orders)
prev_amount = round(sum(float(o.total_amount or 0) for o in prev_day_orders), 2)
# 未解决的配送时效预警(可按区域通过任务表关联)
warn_q = (
db.query(BizDeliveryWarning)
.join(
BizDeliveryTask,
and_(
BizDeliveryTask.id == BizDeliveryWarning.task_id,
BizDeliveryTask.is_del == 0,
),
)
.join(SysRegion, BizDeliveryTask.region_id == SysRegion.id)
.filter(
and_(
BizDeliveryWarning.is_del == 0,
BizDeliveryWarning.is_resolved == 0,
SysRegion.is_del == 0,
*region_active_clause,
)
)
)
if region_id:
warn_q = warn_q.filter(BizDeliveryTask.region_id == region_id)
delivery_warning_count = warn_q.count()
# 今日新提交报价:状态为已提交,且提交时间(无则用创建时间)落在当日 0 点~次日 0 点
quot_submitted_at = func.coalesce(BizQuotation.submit_time, BizQuotation.create_at)
sub_quot_q = (
db.query(BizQuotation)
.join(SysRegion, BizQuotation.region_id == SysRegion.id)
.filter(
and_(
BizQuotation.is_del == 0,
BizQuotation.status == "submitted",
quot_submitted_at >= today,
quot_submitted_at < tomorrow_start,
SysRegion.is_del == 0,
*region_active_clause,
)
)
)
if region_id:
sub_quot_q = sub_quot_q.filter(BizQuotation.region_id == region_id)
pending_quotation_count = sub_quot_q.count()
sub_quot_preview = (
db.query(BizQuotation, BizInquiry)
.join(BizInquiry, BizInquiry.id == BizQuotation.inquiry_id)
.join(SysRegion, BizQuotation.region_id == SysRegion.id)
.filter(
BizQuotation.is_del == 0,
BizQuotation.status == "submitted",
quot_submitted_at >= today,
quot_submitted_at < tomorrow_start,
BizInquiry.is_del == 0,
SysRegion.is_del == 0,
*region_active_clause,
*([BizQuotation.region_id == region_id] if region_id else []),
)
.order_by(desc(BizQuotation.submit_time), desc(BizQuotation.create_at))
.limit(2)
.all()
)
quotation_lines = []
for q, inq in sub_quot_preview:
title = (inq.title or "询价单")[:48]
quotation_lines.append(title)
pd_preview = (
db.query(BizOrder)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(
and_(
BizOrder.is_del == 0,
BizOrder.delivery_status == "pending_dispatch",
SysRegion.is_del == 0,
*region_active_clause,
*region_clause,
)
)
.order_by(desc(BizOrder.create_at))
.limit(2)
.all()
)
delivery_lines = [o.order_no for o in pd_preview]
cancel_preview = (
db.query(BizOrder)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(
and_(
BizOrder.is_del == 0,
BizOrder.cancel_status == "pending_review",
SysRegion.is_del == 0,
*region_active_clause,
*region_clause,
)
)
.order_by(desc(BizOrder.update_at))
.limit(2)
.all()
)
cancel_client_ids = list({o.client_id for o in cancel_preview if o.client_id})
cancel_clients = {}
if cancel_client_ids:
for u in (
db.query(SysUser)
.filter(and_(SysUser.is_del == 0, SysUser.id.in_(cancel_client_ids)))
.all()
):
cancel_clients[u.id] = u.real_name or u.phone or u.id
cancel_lines = []
for o in cancel_preview:
nm = cancel_clients.get(o.client_id, "")
cancel_lines.append(f"{o.order_no}" + (f" · {nm[:16]}" if nm else ""))
settle_preview = (
db.query(BizOrder)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(
and_(
BizOrder.is_del == 0,
BizOrder.client_settlement_status == "pending",
BizOrder.pay_status == "paid",
BizOrder.order_status.in_(["pending_confirm", "completed"]),
SysRegion.is_del == 0,
*region_active_clause,
*region_clause,
)
)
.order_by(desc(BizOrder.update_at))
.limit(2)
.all()
)
settle_client_ids = list({o.client_id for o in settle_preview if o.client_id})
settle_clients = {}
if settle_client_ids:
for u in (
db.query(SysUser)
.filter(and_(SysUser.is_del == 0, SysUser.id.in_(settle_client_ids)))
.all()
):
settle_clients[u.id] = u.real_name or u.phone or u.id
settle_lines = []
for o in settle_preview:
nm = settle_clients.get(o.client_id, "")
settle_lines.append(f"{o.order_no}" + (f" · {nm[:16]}" if nm else ""))
pending_settlement_count = (
db.query(BizOrder)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(
and_(
BizOrder.is_del == 0,
BizOrder.client_settlement_status == "pending",
BizOrder.pay_status == "paid",
BizOrder.order_status.in_(["pending_confirm", "completed"]),
SysRegion.is_del == 0,
*region_active_clause,
*region_clause,
)
)
.count()
)
pending_cancel_count = (
db.query(BizOrder)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(
and_(
BizOrder.is_del == 0,
BizOrder.cancel_status == "pending_review",
SysRegion.is_del == 0,
*region_active_clause,
*region_clause,
)
)
.count()
)
# 最新订单(工作台列表)
recent_rows = (
db.query(BizOrder)
.join(SysRegion, BizOrder.region_id == SysRegion.id)
.filter(and_(BizOrder.is_del == 0, SysRegion.is_del == 0, *region_active_clause, *region_clause))
.order_by(desc(BizOrder.create_at))
.limit(8)
.all()
)
rid_set = list({o.region_id for o in recent_rows if o.region_id})
cid_set = list({o.client_id for o in recent_rows if o.client_id})
region_name_map = {}
if rid_set:
for r in db.query(SysRegion).filter(
and_(SysRegion.is_del == 0, *region_active_clause, SysRegion.id.in_(rid_set))
).all():
region_name_map[r.id] = r.name
client_name_map = {}
if cid_set:
for u in db.query(SysUser).filter(and_(SysUser.is_del == 0, SysUser.id.in_(cid_set))).all():
client_name_map[u.id] = u.real_name or u.phone or "-"
recent_orders = []
for o in recent_rows:
order_status = getattr(o, "order_status", None) or "pending_payment"
recent_orders.append(
{
"id": o.id,
"orderNo": o.order_no,
"clientName": client_name_map.get(o.client_id, "-"),
"regionName": region_name_map.get(o.region_id, "-"),
"totalAmount": float(o.total_amount) if o.total_amount else 0,
"orderStatus": order_status,
"cancelStatus": getattr(o, "cancel_status", None) or "none",
"deliveryStatus": getattr(o, "delivery_status", None) or "pending_dispatch",
"createAt": str(o.create_at) if o.create_at else None,
}
)
todo_snapshots = [
{
"type": "quotation",
"title": "今日新提交报价",
"count": pending_quotation_count,
"items": quotation_lines,
},
{
"type": "delivery",
"title": "待分配配送",
"count": pending_delivery_count,
"items": delivery_lines,
},
{
"type": "cancel",
"title": "取消待审核",
"count": pending_cancel_count,
"items": cancel_lines,
},
{
"type": "settlement",
"title": "待客户结算",
"count": pending_settlement_count,
"items": settle_lines,
},
]
# “客户总数/供应商总数”口径:统计用户表(sys_user)并按 region_id 区分
# - region_id 为空:不过滤全局
# - region_id 传入:按 SysUser.region_id 过滤
customer_count = (
db.query(SysUser)
.join(SysRegion, SysUser.region_id == SysRegion.id)
.filter(
and_(
SysUser.is_del == 0,
SysUser.role == "client",
SysRegion.is_del == 0,
*region_active_clause,
*([SysUser.region_id == region_id] if region_id else []),
)
)
.count()
)
supplier_count = (
db.query(SysUser)
.join(SysRegion, SysUser.region_id == SysRegion.id)
.filter(
and_(
SysUser.is_del == 0,
SysUser.role == "supplier",
SysRegion.is_del == 0,
*region_active_clause,
*([SysUser.region_id == region_id] if region_id else []),
)
)
.count()
)
return {
"todayOrderCount": today_order_count,
"todayOrderAmount": today_order_amount,
"monthOrderCount": len(month_orders),
"monthOrderAmount": round(sum(float(o.total_amount or 0) for o in month_orders), 2),
"pendingDeliveryCount": pending_delivery_count,
"completedOrderCount": sum(
1 for o in month_orders if (getattr(o, "order_status", None) or o.status) == "completed"
),
"activePriceListCount": (
db.query(BizPriceList)
.join(SysRegion, BizPriceList.region_id == SysRegion.id)
.filter(
and_(
BizPriceList.is_del == 0,
BizPriceList.status == "active",
SysRegion.is_del == 0,
*region_active_clause,
*([BizPriceList.region_id == region_id] if region_id else []),
)
)
.count()
),
"customerCount": customer_count,
"supplierCount": supplier_count,
"todayOrderCountHint": _delta_hint(today_order_count, prev_count, " 单"),
"todayOrderAmountHint": _amount_delta_hint(today_order_amount, prev_amount),
"deliveryWarningCount": delivery_warning_count,
"recentOrders": recent_orders,
"todoSnapshots": todo_snapshots,
}
statistics_crud = StatisticsCRUD()