statistics.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. """数据统计 CRUD"""
  2. from sqlalchemy.orm import Session
  3. from sqlalchemy import and_, func, desc, extract
  4. from typing import Optional, List, Dict, Any, Tuple
  5. from datetime import datetime, timedelta
  6. from decimal import Decimal
  7. from app.models.models import (
  8. BizOrder, BizOrderItem, BizDeliveryTask,
  9. BizSettlement, BizRefund, BizPriceList, BizPriceListItem,
  10. SysUser, SysRegion, BasProduct
  11. )
  12. class StatisticsCRUD:
  13. """数据统计CRUD操作"""
  14. @staticmethod
  15. def get_order_statistics(
  16. db: Session,
  17. region_id: str = None,
  18. start_date: str = None,
  19. end_date: str = None
  20. ) -> Dict[str, Any]:
  21. """获取订单统计数据"""
  22. query = db.query(BizOrder).filter(BizOrder.is_del == 0)
  23. if region_id:
  24. query = query.filter(BizOrder.region_id == region_id)
  25. if start_date:
  26. try:
  27. start_dt = datetime.strptime(start_date, "%Y-%m-%d")
  28. query = query.filter(BizOrder.create_at >= start_dt)
  29. except:
  30. pass
  31. if end_date:
  32. try:
  33. end_dt = datetime.strptime(end_date, "%Y-%m-%d")
  34. end_dt = end_dt.replace(hour=23, minute=59, second=59)
  35. query = query.filter(BizOrder.create_at <= end_dt)
  36. except:
  37. pass
  38. orders = query.all()
  39. total_orders = len(orders)
  40. pending_payment = sum(1 for o in orders if o.status == "pending_payment")
  41. pending_dispatch = sum(1 for o in orders if getattr(o, "delivery_status", None) == "pending_dispatch")
  42. delivering = sum(1 for o in orders if o.status == "delivering")
  43. completed = sum(1 for o in orders if o.status == "completed")
  44. cancelled = sum(1 for o in orders if o.status == "cancelled")
  45. total_amount = sum(float(o.total_amount or 0) for o in orders)
  46. paid_amount = sum(float(o.paid_amount or 0) for o in orders)
  47. refund_amount = sum(float(o.refund_amount or 0) for o in orders)
  48. return {
  49. "totalOrders": total_orders,
  50. "pendingPayment": pending_payment,
  51. "pendingDelivery": pending_dispatch,
  52. "delivering": delivering,
  53. "completed": completed,
  54. "cancelled": cancelled,
  55. "totalAmount": round(total_amount, 2),
  56. "paidAmount": round(paid_amount, 2),
  57. "refundAmount": round(refund_amount, 2)
  58. }
  59. @staticmethod
  60. def get_order_trend(
  61. db: Session,
  62. region_id: str = None,
  63. days: int = 7
  64. ) -> List[Dict[str, Any]]:
  65. """获取订单趋势数据(按日统计)"""
  66. result = []
  67. today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
  68. for i in range(days - 1, -1, -1):
  69. day_start = today - timedelta(days=i)
  70. day_end = day_start + timedelta(days=1)
  71. query = db.query(BizOrder).filter(
  72. and_(
  73. BizOrder.is_del == 0,
  74. BizOrder.create_at >= day_start,
  75. BizOrder.create_at < day_end
  76. )
  77. )
  78. if region_id:
  79. query = query.filter(BizOrder.region_id == region_id)
  80. orders = query.all()
  81. order_count = len(orders)
  82. order_amount = sum(float(o.total_amount or 0) for o in orders)
  83. result.append({
  84. "date": day_start.strftime("%Y-%m-%d"),
  85. "orderCount": order_count,
  86. "orderAmount": round(order_amount, 2)
  87. })
  88. return result
  89. @staticmethod
  90. def get_financial_statistics(
  91. db: Session,
  92. region_id: str = None,
  93. start_date: str = None,
  94. end_date: str = None
  95. ) -> Dict[str, Any]:
  96. """获取财务统计数据"""
  97. order_query = db.query(BizOrder).filter(BizOrder.is_del == 0)
  98. settlement_query = db.query(BizSettlement).filter(BizSettlement.is_del == 0)
  99. refund_query = db.query(BizRefund).filter(BizRefund.is_del == 0)
  100. if region_id:
  101. order_query = order_query.filter(BizOrder.region_id == region_id)
  102. settlement_query = settlement_query.filter(BizSettlement.region_id == region_id)
  103. if start_date:
  104. try:
  105. start_dt = datetime.strptime(start_date, "%Y-%m-%d")
  106. order_query = order_query.filter(BizOrder.create_at >= start_dt)
  107. settlement_query = settlement_query.filter(BizSettlement.create_at >= start_dt)
  108. refund_query = refund_query.filter(BizRefund.create_at >= start_dt)
  109. except:
  110. pass
  111. if end_date:
  112. try:
  113. end_dt = datetime.strptime(end_date, "%Y-%m-%d")
  114. end_dt = end_dt.replace(hour=23, minute=59, second=59)
  115. order_query = order_query.filter(BizOrder.create_at <= end_dt)
  116. settlement_query = settlement_query.filter(BizSettlement.create_at <= end_dt)
  117. refund_query = refund_query.filter(BizRefund.create_at <= end_dt)
  118. except:
  119. pass
  120. orders = order_query.all()
  121. settlements = settlement_query.filter(BizSettlement.status == "confirmed").all()
  122. refunds = refund_query.filter(BizRefund.status == "completed").all()
  123. total_revenue = sum(float(o.total_amount or 0) for o in orders)
  124. total_received = sum(float(s.amount or 0) for s in settlements)
  125. total_refund = sum(float(r.refund_amount or 0) for r in refunds)
  126. net_revenue = total_received - total_refund
  127. return {
  128. "totalRevenue": round(total_revenue, 2),
  129. "totalReceived": round(total_received, 2),
  130. "totalRefund": round(total_refund, 2),
  131. "netRevenue": round(net_revenue, 2),
  132. "orderCount": len(orders),
  133. "settlementCount": len(settlements),
  134. "refundCount": len(refunds)
  135. }
  136. @staticmethod
  137. def get_financial_trend(
  138. db: Session,
  139. region_id: str = None,
  140. days: int = 7
  141. ) -> List[Dict[str, Any]]:
  142. """获取财务趋势数据(按日统计)"""
  143. result = []
  144. today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
  145. for i in range(days - 1, -1, -1):
  146. day_start = today - timedelta(days=i)
  147. day_end = day_start + timedelta(days=1)
  148. settlement_query = db.query(BizSettlement).filter(
  149. and_(
  150. BizSettlement.is_del == 0,
  151. BizSettlement.status == "confirmed",
  152. BizSettlement.create_at >= day_start,
  153. BizSettlement.create_at < day_end
  154. )
  155. )
  156. refund_query = db.query(BizRefund).filter(
  157. and_(
  158. BizRefund.is_del == 0,
  159. BizRefund.status == "completed",
  160. BizRefund.create_at >= day_start,
  161. BizRefund.create_at < day_end
  162. )
  163. )
  164. if region_id:
  165. settlement_query = settlement_query.filter(BizSettlement.region_id == region_id)
  166. settlements = settlement_query.all()
  167. refunds = refund_query.all()
  168. received = sum(float(s.amount or 0) for s in settlements)
  169. refund = sum(float(r.refund_amount or 0) for r in refunds)
  170. result.append({
  171. "date": day_start.strftime("%Y-%m-%d"),
  172. "received": round(received, 2),
  173. "refund": round(refund, 2),
  174. "net": round(received - refund, 2)
  175. })
  176. return result
  177. @staticmethod
  178. def get_delivery_statistics(
  179. db: Session,
  180. region_id: str = None,
  181. start_date: str = None,
  182. end_date: str = None
  183. ) -> Dict[str, Any]:
  184. """获取配送统计数据"""
  185. query = db.query(BizDeliveryTask).filter(BizDeliveryTask.is_del == 0)
  186. if region_id:
  187. query = query.filter(BizDeliveryTask.region_id == region_id)
  188. if start_date:
  189. try:
  190. start_dt = datetime.strptime(start_date, "%Y-%m-%d")
  191. query = query.filter(BizDeliveryTask.create_at >= start_dt)
  192. except:
  193. pass
  194. if end_date:
  195. try:
  196. end_dt = datetime.strptime(end_date, "%Y-%m-%d")
  197. end_dt = end_dt.replace(hour=23, minute=59, second=59)
  198. query = query.filter(BizDeliveryTask.create_at <= end_dt)
  199. except:
  200. pass
  201. tasks = query.all()
  202. pending = sum(1 for t in tasks if t.status == "pending")
  203. picked_up = sum(1 for t in tasks if t.status == "picked_up")
  204. delivering = sum(1 for t in tasks if t.status == "delivering")
  205. completed = sum(1 for t in tasks if t.status == "completed")
  206. cancelled = sum(1 for t in tasks if t.status == "cancelled")
  207. return {
  208. "totalTasks": len(tasks),
  209. "pending": pending,
  210. "pickedUp": picked_up,
  211. "delivering": delivering,
  212. "completed": completed,
  213. "cancelled": cancelled
  214. }
  215. @staticmethod
  216. def get_product_statistics(
  217. db: Session,
  218. region_id: str = None,
  219. start_date: str = None,
  220. end_date: str = None,
  221. limit: int = 10
  222. ) -> List[Dict[str, Any]]:
  223. """获取商品销售排行"""
  224. query = db.query(
  225. BizOrderItem.product_id,
  226. func.sum(BizOrderItem.quantity).label("total_quantity"),
  227. func.sum(BizOrderItem.amount).label("total_amount")
  228. ).join(
  229. BizOrder,
  230. and_(
  231. BizOrder.id == BizOrderItem.order_id,
  232. BizOrder.is_del == 0,
  233. BizOrder.status.in_(["completed", "delivering", "pending_confirm"])
  234. )
  235. ).filter(
  236. BizOrderItem.is_del == 0
  237. )
  238. if region_id:
  239. query = query.filter(BizOrder.region_id == region_id)
  240. if start_date:
  241. try:
  242. start_dt = datetime.strptime(start_date, "%Y-%m-%d")
  243. query = query.filter(BizOrder.create_at >= start_dt)
  244. except:
  245. pass
  246. if end_date:
  247. try:
  248. end_dt = datetime.strptime(end_date, "%Y-%m-%d")
  249. end_dt = end_dt.replace(hour=23, minute=59, second=59)
  250. query = query.filter(BizOrder.create_at <= end_dt)
  251. except:
  252. pass
  253. results = query.group_by(BizOrderItem.product_id).order_by(
  254. desc("total_amount")
  255. ).limit(limit).all()
  256. product_stats = []
  257. for r in results:
  258. product = db.query(BasProduct).filter(
  259. and_(BasProduct.id == r.product_id, BasProduct.is_del == 0)
  260. ).first()
  261. product_stats.append({
  262. "productId": r.product_id,
  263. "productName": product.name if product else None,
  264. "totalQuantity": float(r.total_quantity or 0),
  265. "totalAmount": float(r.total_amount or 0)
  266. })
  267. return product_stats
  268. @staticmethod
  269. def get_customer_statistics(
  270. db: Session,
  271. region_id: str = None,
  272. start_date: str = None,
  273. end_date: str = None,
  274. limit: int = 10
  275. ) -> List[Dict[str, Any]]:
  276. """获取客户订单排行"""
  277. query = db.query(
  278. BizOrder.client_id,
  279. func.count(BizOrder.id).label("order_count"),
  280. func.sum(BizOrder.total_amount).label("total_amount")
  281. ).filter(
  282. and_(
  283. BizOrder.is_del == 0,
  284. BizOrder.status.in_(["completed", "delivering", "pending_confirm", "pending_payment"])
  285. )
  286. )
  287. if region_id:
  288. query = query.filter(BizOrder.region_id == region_id)
  289. if start_date:
  290. try:
  291. start_dt = datetime.strptime(start_date, "%Y-%m-%d")
  292. query = query.filter(BizOrder.create_at >= start_dt)
  293. except:
  294. pass
  295. if end_date:
  296. try:
  297. end_dt = datetime.strptime(end_date, "%Y-%m-%d")
  298. end_dt = end_dt.replace(hour=23, minute=59, second=59)
  299. query = query.filter(BizOrder.create_at <= end_dt)
  300. except:
  301. pass
  302. results = query.group_by(BizOrder.client_id).order_by(
  303. desc("total_amount")
  304. ).limit(limit).all()
  305. customer_stats = []
  306. for r in results:
  307. customer = db.query(SysUser).filter(
  308. and_(SysUser.id == r.client_id, SysUser.is_del == 0)
  309. ).first()
  310. customer_stats.append({
  311. "customerId": r.client_id,
  312. "customerName": customer.real_name if customer else None,
  313. "customerPhone": customer.phone if customer else None,
  314. "orderCount": r.order_count,
  315. "totalAmount": float(r.total_amount or 0)
  316. })
  317. return customer_stats
  318. @staticmethod
  319. def get_dashboard_summary(
  320. db: Session,
  321. region_id: str = None
  322. ) -> Dict[str, Any]:
  323. """获取仪表盘汇总数据"""
  324. today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
  325. month_start = today.replace(day=1)
  326. order_query = db.query(BizOrder).filter(
  327. and_(BizOrder.is_del == 0, BizOrder.create_at >= month_start)
  328. )
  329. if region_id:
  330. order_query = order_query.filter(BizOrder.region_id == region_id)
  331. month_orders = order_query.all()
  332. today_orders = [o for o in month_orders if o.create_at >= today]
  333. pending_delivery_count = db.query(BizOrder).filter(
  334. and_(
  335. BizOrder.is_del == 0,
  336. BizOrder.delivery_status == "pending_dispatch"
  337. )
  338. )
  339. if region_id:
  340. pending_delivery_count = pending_delivery_count.filter(BizOrder.region_id == region_id)
  341. return {
  342. "todayOrderCount": len(today_orders),
  343. "todayOrderAmount": round(sum(float(o.total_amount or 0) for o in today_orders), 2),
  344. "monthOrderCount": len(month_orders),
  345. "monthOrderAmount": round(sum(float(o.total_amount or 0) for o in month_orders), 2),
  346. "pendingDeliveryCount": pending_delivery_count.count(),
  347. "completedOrderCount": sum(1 for o in month_orders if o.status == "completed"),
  348. "activePriceListCount": db.query(BizPriceList).filter(
  349. and_(BizPriceList.is_del == 0, BizPriceList.status == "active")
  350. ).count(),
  351. "customerCount": db.query(SysUser).filter(
  352. and_(SysUser.is_del == 0, SysUser.role == "client")
  353. ).count(),
  354. "supplierCount": db.query(SysUser).filter(
  355. and_(SysUser.is_del == 0, SysUser.role == "supplier")
  356. ).count()
  357. }
  358. statistics_crud = StatisticsCRUD()