statistics.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  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. BizInquiry, BizQuotation, BizDeliveryWarning,
  12. )
  13. class StatisticsCRUD:
  14. """数据统计CRUD操作"""
  15. @staticmethod
  16. def get_order_statistics(
  17. db: Session,
  18. region_id: str = None,
  19. start_date: str = None,
  20. end_date: str = None
  21. ) -> Dict[str, Any]:
  22. """获取订单统计数据"""
  23. query = db.query(BizOrder).filter(BizOrder.is_del == 0)
  24. if region_id:
  25. query = query.filter(BizOrder.region_id == region_id)
  26. if start_date:
  27. try:
  28. start_dt = datetime.strptime(start_date, "%Y-%m-%d")
  29. query = query.filter(BizOrder.create_at >= start_dt)
  30. except:
  31. pass
  32. if end_date:
  33. try:
  34. end_dt = datetime.strptime(end_date, "%Y-%m-%d")
  35. end_dt = end_dt.replace(hour=23, minute=59, second=59)
  36. query = query.filter(BizOrder.create_at <= end_dt)
  37. except:
  38. pass
  39. orders = query.all()
  40. total_orders = len(orders)
  41. pending_payment = sum(1 for o in orders if o.status == "pending_payment")
  42. pending_dispatch = sum(1 for o in orders if getattr(o, "delivery_status", None) == "pending_dispatch")
  43. delivering = sum(1 for o in orders if o.status == "delivering")
  44. completed = sum(1 for o in orders if o.status == "completed")
  45. cancelled = sum(1 for o in orders if o.status == "cancelled")
  46. total_amount = sum(float(o.total_amount or 0) for o in orders)
  47. paid_amount = sum(float(o.paid_amount or 0) for o in orders)
  48. refund_amount = sum(float(o.refund_amount or 0) for o in orders)
  49. return {
  50. "totalOrders": total_orders,
  51. "pendingPayment": pending_payment,
  52. "pendingDelivery": pending_dispatch,
  53. "delivering": delivering,
  54. "completed": completed,
  55. "cancelled": cancelled,
  56. "totalAmount": round(total_amount, 2),
  57. "paidAmount": round(paid_amount, 2),
  58. "refundAmount": round(refund_amount, 2)
  59. }
  60. @staticmethod
  61. def get_order_trend(
  62. db: Session,
  63. region_id: str = None,
  64. days: int = 7,
  65. include_inactive_regions: bool = False,
  66. ) -> List[Dict[str, Any]]:
  67. """获取订单趋势数据(按日统计)"""
  68. result = []
  69. today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
  70. region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
  71. for i in range(days - 1, -1, -1):
  72. day_start = today - timedelta(days=i)
  73. day_end = day_start + timedelta(days=1)
  74. query = (
  75. db.query(BizOrder)
  76. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  77. .filter(
  78. and_(
  79. BizOrder.is_del == 0,
  80. BizOrder.create_at >= day_start,
  81. BizOrder.create_at < day_end,
  82. SysRegion.is_del == 0,
  83. *region_active_clause,
  84. )
  85. )
  86. )
  87. if region_id:
  88. query = query.filter(BizOrder.region_id == region_id)
  89. orders = query.all()
  90. order_count = len(orders)
  91. order_amount = sum(float(o.total_amount or 0) for o in orders)
  92. result.append({
  93. "date": day_start.strftime("%Y-%m-%d"),
  94. "orderCount": order_count,
  95. "orderAmount": round(order_amount, 2)
  96. })
  97. return result
  98. @staticmethod
  99. def get_today_order_stats_by_region(
  100. db: Session,
  101. region_id: str = None,
  102. include_inactive_regions: bool = False,
  103. ) -> List[Dict[str, Any]]:
  104. """今日各区域营业额(元)、订单笔数;列出未删除的区域,无单则为 0。"""
  105. today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
  106. day_end = today + timedelta(days=1)
  107. region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
  108. reg_q = db.query(SysRegion).filter(
  109. SysRegion.is_del == 0,
  110. )
  111. if region_id:
  112. reg_q = reg_q.filter(SysRegion.id == region_id)
  113. regions = reg_q.order_by(SysRegion.sort_order, SysRegion.name).all()
  114. agg_q = (
  115. db.query(
  116. BizOrder.region_id,
  117. func.count(BizOrder.id).label("order_count"),
  118. func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"),
  119. )
  120. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  121. .filter(
  122. BizOrder.is_del == 0,
  123. BizOrder.create_at >= today,
  124. BizOrder.create_at < day_end,
  125. SysRegion.is_del == 0,
  126. *region_active_clause,
  127. )
  128. .group_by(BizOrder.region_id)
  129. )
  130. if region_id:
  131. agg_q = agg_q.filter(BizOrder.region_id == region_id)
  132. agg_map: Dict[str, Any] = {r.region_id: r for r in agg_q.all() if r.region_id}
  133. out: List[Dict[str, Any]] = []
  134. for reg in regions:
  135. row = agg_map.get(reg.id)
  136. cnt = int(row.order_count or 0) if row else 0
  137. amt = float(row.order_amount or 0) if row else 0.0
  138. out.append({
  139. "regionId": reg.id,
  140. "regionName": reg.name or reg.id,
  141. "orderCount": cnt,
  142. "orderAmount": round(amt, 2),
  143. })
  144. return out
  145. @staticmethod
  146. def get_today_order_stats_by_client(
  147. db: Session,
  148. region_id: str = None,
  149. limit: int = 30,
  150. include_inactive_regions: bool = False,
  151. ) -> List[Dict[str, Any]]:
  152. """今日各下单客户:下单金额合计(元)、商品数量合计(订单行数量之和);按金额降序,最多 limit 条。"""
  153. today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
  154. day_end = today + timedelta(days=1)
  155. region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
  156. amt_q = (
  157. db.query(
  158. BizOrder.client_id,
  159. func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"),
  160. )
  161. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  162. .filter(
  163. BizOrder.is_del == 0,
  164. BizOrder.create_at >= today,
  165. BizOrder.create_at < day_end,
  166. SysRegion.is_del == 0,
  167. *region_active_clause,
  168. )
  169. )
  170. if region_id:
  171. amt_q = amt_q.filter(BizOrder.region_id == region_id)
  172. amt_rows = amt_q.group_by(BizOrder.client_id).all()
  173. qty_q = (
  174. db.query(
  175. BizOrder.client_id,
  176. func.coalesce(func.sum(BizOrderItem.quantity), 0).label("qty"),
  177. )
  178. .select_from(BizOrderItem)
  179. .join(
  180. BizOrder,
  181. and_(
  182. BizOrder.id == BizOrderItem.order_id,
  183. BizOrder.is_del == 0,
  184. BizOrder.create_at >= today,
  185. BizOrder.create_at < day_end,
  186. ),
  187. )
  188. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  189. .filter(BizOrderItem.is_del == 0)
  190. .filter(SysRegion.is_del == 0, *region_active_clause)
  191. )
  192. if region_id:
  193. qty_q = qty_q.filter(BizOrder.region_id == region_id)
  194. qty_rows = qty_q.group_by(BizOrder.client_id).all()
  195. qty_map: Dict[str, float] = {
  196. r.client_id: float(r.qty or 0) for r in qty_rows if r.client_id
  197. }
  198. merged: List[Dict[str, Any]] = []
  199. for r in amt_rows:
  200. cid = r.client_id
  201. if not cid:
  202. continue
  203. merged.append({
  204. "clientId": cid,
  205. "orderAmount": round(float(r.order_amount or 0), 2),
  206. "productQuantity": round(float(qty_map.get(cid, 0) or 0), 4),
  207. })
  208. merged.sort(key=lambda x: -x["orderAmount"])
  209. merged = merged[: max(1, min(int(limit or 30), 100))]
  210. client_ids = [m["clientId"] for m in merged]
  211. names: Dict[str, str] = {}
  212. if client_ids:
  213. for u in (
  214. db.query(SysUser)
  215. .filter(and_(SysUser.is_del == 0, SysUser.id.in_(client_ids)))
  216. .all()
  217. ):
  218. names[u.id] = (u.real_name or u.phone or u.username or u.id) or u.id
  219. out: List[Dict[str, Any]] = []
  220. for m in merged:
  221. cid = m["clientId"]
  222. nm = names.get(cid, cid)
  223. out.append({
  224. "clientId": cid,
  225. "clientName": nm,
  226. "orderAmount": m["orderAmount"],
  227. "productQuantity": m["productQuantity"],
  228. })
  229. return out
  230. @staticmethod
  231. def get_financial_statistics(
  232. db: Session,
  233. region_id: str = None,
  234. start_date: str = None,
  235. end_date: str = None
  236. ) -> Dict[str, Any]:
  237. """获取财务统计数据"""
  238. order_query = db.query(BizOrder).filter(BizOrder.is_del == 0)
  239. settlement_query = db.query(BizSettlement).filter(BizSettlement.is_del == 0)
  240. refund_query = db.query(BizRefund).filter(BizRefund.is_del == 0)
  241. if region_id:
  242. order_query = order_query.filter(BizOrder.region_id == region_id)
  243. settlement_query = settlement_query.filter(BizSettlement.region_id == region_id)
  244. if start_date:
  245. try:
  246. start_dt = datetime.strptime(start_date, "%Y-%m-%d")
  247. order_query = order_query.filter(BizOrder.create_at >= start_dt)
  248. settlement_query = settlement_query.filter(BizSettlement.create_at >= start_dt)
  249. refund_query = refund_query.filter(BizRefund.create_at >= start_dt)
  250. except:
  251. pass
  252. if end_date:
  253. try:
  254. end_dt = datetime.strptime(end_date, "%Y-%m-%d")
  255. end_dt = end_dt.replace(hour=23, minute=59, second=59)
  256. order_query = order_query.filter(BizOrder.create_at <= end_dt)
  257. settlement_query = settlement_query.filter(BizSettlement.create_at <= end_dt)
  258. refund_query = refund_query.filter(BizRefund.create_at <= end_dt)
  259. except:
  260. pass
  261. orders = order_query.all()
  262. settlements = settlement_query.filter(BizSettlement.status == "confirmed").all()
  263. refunds = refund_query.filter(BizRefund.status == "completed").all()
  264. total_revenue = sum(float(o.total_amount or 0) for o in orders)
  265. total_received = sum(float(s.amount or 0) for s in settlements)
  266. total_refund = sum(float(r.refund_amount or 0) for r in refunds)
  267. net_revenue = total_received - total_refund
  268. return {
  269. "totalRevenue": round(total_revenue, 2),
  270. "totalReceived": round(total_received, 2),
  271. "totalRefund": round(total_refund, 2),
  272. "netRevenue": round(net_revenue, 2),
  273. "orderCount": len(orders),
  274. "settlementCount": len(settlements),
  275. "refundCount": len(refunds)
  276. }
  277. @staticmethod
  278. def get_financial_trend(
  279. db: Session,
  280. region_id: str = None,
  281. days: int = 7
  282. ) -> List[Dict[str, Any]]:
  283. """获取财务趋势数据(按日统计)"""
  284. result = []
  285. today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
  286. for i in range(days - 1, -1, -1):
  287. day_start = today - timedelta(days=i)
  288. day_end = day_start + timedelta(days=1)
  289. settlement_query = db.query(BizSettlement).filter(
  290. and_(
  291. BizSettlement.is_del == 0,
  292. BizSettlement.status == "confirmed",
  293. BizSettlement.create_at >= day_start,
  294. BizSettlement.create_at < day_end
  295. )
  296. )
  297. refund_query = db.query(BizRefund).filter(
  298. and_(
  299. BizRefund.is_del == 0,
  300. BizRefund.status == "completed",
  301. BizRefund.create_at >= day_start,
  302. BizRefund.create_at < day_end
  303. )
  304. )
  305. if region_id:
  306. settlement_query = settlement_query.filter(BizSettlement.region_id == region_id)
  307. settlements = settlement_query.all()
  308. refunds = refund_query.all()
  309. received = sum(float(s.amount or 0) for s in settlements)
  310. refund = sum(float(r.refund_amount or 0) for r in refunds)
  311. result.append({
  312. "date": day_start.strftime("%Y-%m-%d"),
  313. "received": round(received, 2),
  314. "refund": round(refund, 2),
  315. "net": round(received - refund, 2)
  316. })
  317. return result
  318. @staticmethod
  319. def get_delivery_statistics(
  320. db: Session,
  321. region_id: str = None,
  322. start_date: str = None,
  323. end_date: str = None
  324. ) -> Dict[str, Any]:
  325. """获取配送统计数据"""
  326. query = db.query(BizDeliveryTask).filter(BizDeliveryTask.is_del == 0)
  327. if region_id:
  328. query = query.filter(BizDeliveryTask.region_id == region_id)
  329. if start_date:
  330. try:
  331. start_dt = datetime.strptime(start_date, "%Y-%m-%d")
  332. query = query.filter(BizDeliveryTask.create_at >= start_dt)
  333. except:
  334. pass
  335. if end_date:
  336. try:
  337. end_dt = datetime.strptime(end_date, "%Y-%m-%d")
  338. end_dt = end_dt.replace(hour=23, minute=59, second=59)
  339. query = query.filter(BizDeliveryTask.create_at <= end_dt)
  340. except:
  341. pass
  342. tasks = query.all()
  343. pending = sum(1 for t in tasks if t.status == "pending")
  344. picked_up = sum(1 for t in tasks if t.status == "picked_up")
  345. delivering = sum(1 for t in tasks if t.status == "delivering")
  346. completed = sum(1 for t in tasks if t.status == "completed")
  347. cancelled = sum(1 for t in tasks if t.status == "cancelled")
  348. return {
  349. "totalTasks": len(tasks),
  350. "pending": pending,
  351. "pickedUp": picked_up,
  352. "delivering": delivering,
  353. "completed": completed,
  354. "cancelled": cancelled
  355. }
  356. @staticmethod
  357. def get_product_statistics(
  358. db: Session,
  359. region_id: str = None,
  360. start_date: str = None,
  361. end_date: str = None,
  362. limit: int = 10
  363. ) -> List[Dict[str, Any]]:
  364. """获取商品销售排行"""
  365. query = db.query(
  366. BizOrderItem.product_id,
  367. func.sum(BizOrderItem.quantity).label("total_quantity"),
  368. func.sum(BizOrderItem.amount).label("total_amount")
  369. ).join(
  370. BizOrder,
  371. and_(
  372. BizOrder.id == BizOrderItem.order_id,
  373. BizOrder.is_del == 0,
  374. BizOrder.status.in_(["completed", "delivering", "pending_confirm"])
  375. )
  376. ).filter(
  377. BizOrderItem.is_del == 0
  378. )
  379. if region_id:
  380. query = query.filter(BizOrder.region_id == region_id)
  381. if start_date:
  382. try:
  383. start_dt = datetime.strptime(start_date, "%Y-%m-%d")
  384. query = query.filter(BizOrder.create_at >= start_dt)
  385. except:
  386. pass
  387. if end_date:
  388. try:
  389. end_dt = datetime.strptime(end_date, "%Y-%m-%d")
  390. end_dt = end_dt.replace(hour=23, minute=59, second=59)
  391. query = query.filter(BizOrder.create_at <= end_dt)
  392. except:
  393. pass
  394. results = query.group_by(BizOrderItem.product_id).order_by(
  395. desc("total_amount")
  396. ).limit(limit).all()
  397. product_stats = []
  398. for r in results:
  399. product = db.query(BasProduct).filter(
  400. and_(BasProduct.id == r.product_id, BasProduct.is_del == 0)
  401. ).first()
  402. product_stats.append({
  403. "productId": r.product_id,
  404. "productName": product.name if product else None,
  405. "totalQuantity": float(r.total_quantity or 0),
  406. "totalAmount": float(r.total_amount or 0)
  407. })
  408. return product_stats
  409. @staticmethod
  410. def get_customer_statistics(
  411. db: Session,
  412. region_id: str = None,
  413. start_date: str = None,
  414. end_date: str = None,
  415. limit: int = 10
  416. ) -> List[Dict[str, Any]]:
  417. """获取客户订单排行"""
  418. query = db.query(
  419. BizOrder.client_id,
  420. func.count(BizOrder.id).label("order_count"),
  421. func.sum(BizOrder.total_amount).label("total_amount")
  422. ).filter(
  423. and_(
  424. BizOrder.is_del == 0,
  425. BizOrder.status.in_(["completed", "delivering", "pending_confirm", "pending_payment"])
  426. )
  427. )
  428. if region_id:
  429. query = query.filter(BizOrder.region_id == region_id)
  430. if start_date:
  431. try:
  432. start_dt = datetime.strptime(start_date, "%Y-%m-%d")
  433. query = query.filter(BizOrder.create_at >= start_dt)
  434. except:
  435. pass
  436. if end_date:
  437. try:
  438. end_dt = datetime.strptime(end_date, "%Y-%m-%d")
  439. end_dt = end_dt.replace(hour=23, minute=59, second=59)
  440. query = query.filter(BizOrder.create_at <= end_dt)
  441. except:
  442. pass
  443. results = query.group_by(BizOrder.client_id).order_by(
  444. desc("total_amount")
  445. ).limit(limit).all()
  446. customer_stats = []
  447. for r in results:
  448. customer = db.query(SysUser).filter(
  449. and_(SysUser.id == r.client_id, SysUser.is_del == 0)
  450. ).first()
  451. customer_stats.append({
  452. "customerId": r.client_id,
  453. "customerName": customer.real_name if customer else None,
  454. "customerPhone": customer.phone if customer else None,
  455. "orderCount": r.order_count,
  456. "totalAmount": float(r.total_amount or 0)
  457. })
  458. return customer_stats
  459. @staticmethod
  460. def _dashboard_order_region_clause(region_id: str = None):
  461. return [BizOrder.region_id == region_id] if region_id else []
  462. @staticmethod
  463. def get_dashboard_summary(
  464. db: Session,
  465. region_id: str = None,
  466. include_inactive_regions: bool = False,
  467. ) -> Dict[str, Any]:
  468. """获取仪表盘汇总数据(运营工作台;含待办摘要、最新订单、预警数等,字段驼峰)"""
  469. today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
  470. tomorrow_start = today + timedelta(days=1)
  471. month_start = today.replace(day=1)
  472. yesterday_start = today - timedelta(days=1)
  473. region_clause = StatisticsCRUD._dashboard_order_region_clause(region_id)
  474. region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
  475. order_query = (
  476. db.query(BizOrder)
  477. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  478. .filter(
  479. and_(
  480. BizOrder.is_del == 0,
  481. BizOrder.create_at >= month_start,
  482. SysRegion.is_del == 0,
  483. *region_active_clause,
  484. *region_clause,
  485. )
  486. )
  487. )
  488. month_orders = order_query.all()
  489. today_orders = [
  490. o for o in month_orders if o.create_at and o.create_at >= today
  491. ]
  492. prev_day_orders = [
  493. o
  494. for o in month_orders
  495. if o.create_at and yesterday_start <= o.create_at < today
  496. ]
  497. pending_delivery_q = (
  498. db.query(BizOrder)
  499. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  500. .filter(
  501. and_(
  502. BizOrder.is_del == 0,
  503. BizOrder.delivery_status == "pending_dispatch",
  504. SysRegion.is_del == 0,
  505. *region_active_clause,
  506. *region_clause,
  507. )
  508. )
  509. )
  510. pending_delivery_count = pending_delivery_q.count()
  511. def _delta_hint(cur_n: int, prev_n: int, unit: str) -> str:
  512. d = cur_n - prev_n
  513. if d > 0:
  514. return f"较昨日 +{d}{unit}"
  515. if d < 0:
  516. return f"较昨日 {d}{unit}"
  517. return "与昨日持平"
  518. def _amount_delta_hint(cur_amt: float, prev_amt: float) -> str:
  519. d = round(cur_amt - prev_amt, 2)
  520. if d > 0.0001:
  521. return f"较昨日 +¥{d:,.2f}"
  522. if d < -0.0001:
  523. return f"较昨日 ¥{d:,.2f}"
  524. return "与昨日持平"
  525. today_order_count = len(today_orders)
  526. today_order_amount = round(sum(float(o.total_amount or 0) for o in today_orders), 2)
  527. prev_count = len(prev_day_orders)
  528. prev_amount = round(sum(float(o.total_amount or 0) for o in prev_day_orders), 2)
  529. # 未解决的配送时效预警(可按区域通过任务表关联)
  530. warn_q = (
  531. db.query(BizDeliveryWarning)
  532. .join(
  533. BizDeliveryTask,
  534. and_(
  535. BizDeliveryTask.id == BizDeliveryWarning.task_id,
  536. BizDeliveryTask.is_del == 0,
  537. ),
  538. )
  539. .join(SysRegion, BizDeliveryTask.region_id == SysRegion.id)
  540. .filter(
  541. and_(
  542. BizDeliveryWarning.is_del == 0,
  543. BizDeliveryWarning.is_resolved == 0,
  544. SysRegion.is_del == 0,
  545. *region_active_clause,
  546. )
  547. )
  548. )
  549. if region_id:
  550. warn_q = warn_q.filter(BizDeliveryTask.region_id == region_id)
  551. delivery_warning_count = warn_q.count()
  552. # 今日新提交报价:状态为已提交,且提交时间(无则用创建时间)落在当日 0 点~次日 0 点
  553. quot_submitted_at = func.coalesce(BizQuotation.submit_time, BizQuotation.create_at)
  554. sub_quot_q = (
  555. db.query(BizQuotation)
  556. .join(SysRegion, BizQuotation.region_id == SysRegion.id)
  557. .filter(
  558. and_(
  559. BizQuotation.is_del == 0,
  560. BizQuotation.status == "submitted",
  561. quot_submitted_at >= today,
  562. quot_submitted_at < tomorrow_start,
  563. SysRegion.is_del == 0,
  564. *region_active_clause,
  565. )
  566. )
  567. )
  568. if region_id:
  569. sub_quot_q = sub_quot_q.filter(BizQuotation.region_id == region_id)
  570. pending_quotation_count = sub_quot_q.count()
  571. sub_quot_preview = (
  572. db.query(BizQuotation, BizInquiry)
  573. .join(BizInquiry, BizInquiry.id == BizQuotation.inquiry_id)
  574. .join(SysRegion, BizQuotation.region_id == SysRegion.id)
  575. .filter(
  576. BizQuotation.is_del == 0,
  577. BizQuotation.status == "submitted",
  578. quot_submitted_at >= today,
  579. quot_submitted_at < tomorrow_start,
  580. BizInquiry.is_del == 0,
  581. SysRegion.is_del == 0,
  582. *region_active_clause,
  583. *([BizQuotation.region_id == region_id] if region_id else []),
  584. )
  585. .order_by(desc(BizQuotation.submit_time), desc(BizQuotation.create_at))
  586. .limit(2)
  587. .all()
  588. )
  589. quotation_lines = []
  590. for q, inq in sub_quot_preview:
  591. title = (inq.title or "询价单")[:48]
  592. quotation_lines.append(title)
  593. pd_preview = (
  594. db.query(BizOrder)
  595. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  596. .filter(
  597. and_(
  598. BizOrder.is_del == 0,
  599. BizOrder.delivery_status == "pending_dispatch",
  600. SysRegion.is_del == 0,
  601. *region_active_clause,
  602. *region_clause,
  603. )
  604. )
  605. .order_by(desc(BizOrder.create_at))
  606. .limit(2)
  607. .all()
  608. )
  609. delivery_lines = [o.order_no for o in pd_preview]
  610. cancel_preview = (
  611. db.query(BizOrder)
  612. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  613. .filter(
  614. and_(
  615. BizOrder.is_del == 0,
  616. BizOrder.cancel_status == "pending_review",
  617. SysRegion.is_del == 0,
  618. *region_active_clause,
  619. *region_clause,
  620. )
  621. )
  622. .order_by(desc(BizOrder.update_at))
  623. .limit(2)
  624. .all()
  625. )
  626. cancel_client_ids = list({o.client_id for o in cancel_preview if o.client_id})
  627. cancel_clients = {}
  628. if cancel_client_ids:
  629. for u in (
  630. db.query(SysUser)
  631. .filter(and_(SysUser.is_del == 0, SysUser.id.in_(cancel_client_ids)))
  632. .all()
  633. ):
  634. cancel_clients[u.id] = u.real_name or u.phone or u.id
  635. cancel_lines = []
  636. for o in cancel_preview:
  637. nm = cancel_clients.get(o.client_id, "")
  638. cancel_lines.append(f"{o.order_no}" + (f" · {nm[:16]}" if nm else ""))
  639. settle_preview = (
  640. db.query(BizOrder)
  641. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  642. .filter(
  643. and_(
  644. BizOrder.is_del == 0,
  645. BizOrder.client_settlement_status == "pending",
  646. BizOrder.pay_status == "paid",
  647. BizOrder.order_status.in_(["pending_confirm", "completed"]),
  648. SysRegion.is_del == 0,
  649. *region_active_clause,
  650. *region_clause,
  651. )
  652. )
  653. .order_by(desc(BizOrder.update_at))
  654. .limit(2)
  655. .all()
  656. )
  657. settle_client_ids = list({o.client_id for o in settle_preview if o.client_id})
  658. settle_clients = {}
  659. if settle_client_ids:
  660. for u in (
  661. db.query(SysUser)
  662. .filter(and_(SysUser.is_del == 0, SysUser.id.in_(settle_client_ids)))
  663. .all()
  664. ):
  665. settle_clients[u.id] = u.real_name or u.phone or u.id
  666. settle_lines = []
  667. for o in settle_preview:
  668. nm = settle_clients.get(o.client_id, "")
  669. settle_lines.append(f"{o.order_no}" + (f" · {nm[:16]}" if nm else ""))
  670. pending_settlement_count = (
  671. db.query(BizOrder)
  672. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  673. .filter(
  674. and_(
  675. BizOrder.is_del == 0,
  676. BizOrder.client_settlement_status == "pending",
  677. BizOrder.pay_status == "paid",
  678. BizOrder.order_status.in_(["pending_confirm", "completed"]),
  679. SysRegion.is_del == 0,
  680. *region_active_clause,
  681. *region_clause,
  682. )
  683. )
  684. .count()
  685. )
  686. pending_cancel_count = (
  687. db.query(BizOrder)
  688. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  689. .filter(
  690. and_(
  691. BizOrder.is_del == 0,
  692. BizOrder.cancel_status == "pending_review",
  693. SysRegion.is_del == 0,
  694. *region_active_clause,
  695. *region_clause,
  696. )
  697. )
  698. .count()
  699. )
  700. # 最新订单(工作台列表)
  701. recent_rows = (
  702. db.query(BizOrder)
  703. .join(SysRegion, BizOrder.region_id == SysRegion.id)
  704. .filter(and_(BizOrder.is_del == 0, SysRegion.is_del == 0, *region_active_clause, *region_clause))
  705. .order_by(desc(BizOrder.create_at))
  706. .limit(8)
  707. .all()
  708. )
  709. rid_set = list({o.region_id for o in recent_rows if o.region_id})
  710. cid_set = list({o.client_id for o in recent_rows if o.client_id})
  711. region_name_map = {}
  712. if rid_set:
  713. for r in db.query(SysRegion).filter(
  714. and_(SysRegion.is_del == 0, *region_active_clause, SysRegion.id.in_(rid_set))
  715. ).all():
  716. region_name_map[r.id] = r.name
  717. client_name_map = {}
  718. if cid_set:
  719. for u in db.query(SysUser).filter(and_(SysUser.is_del == 0, SysUser.id.in_(cid_set))).all():
  720. client_name_map[u.id] = u.real_name or u.phone or "-"
  721. recent_orders = []
  722. for o in recent_rows:
  723. order_status = getattr(o, "order_status", None) or "pending_payment"
  724. recent_orders.append(
  725. {
  726. "id": o.id,
  727. "orderNo": o.order_no,
  728. "clientName": client_name_map.get(o.client_id, "-"),
  729. "regionName": region_name_map.get(o.region_id, "-"),
  730. "totalAmount": float(o.total_amount) if o.total_amount else 0,
  731. "orderStatus": order_status,
  732. "cancelStatus": getattr(o, "cancel_status", None) or "none",
  733. "deliveryStatus": getattr(o, "delivery_status", None) or "pending_dispatch",
  734. "createAt": str(o.create_at) if o.create_at else None,
  735. }
  736. )
  737. todo_snapshots = [
  738. {
  739. "type": "quotation",
  740. "title": "今日新提交报价",
  741. "count": pending_quotation_count,
  742. "items": quotation_lines,
  743. },
  744. {
  745. "type": "delivery",
  746. "title": "待分配配送",
  747. "count": pending_delivery_count,
  748. "items": delivery_lines,
  749. },
  750. {
  751. "type": "cancel",
  752. "title": "取消待审核",
  753. "count": pending_cancel_count,
  754. "items": cancel_lines,
  755. },
  756. {
  757. "type": "settlement",
  758. "title": "待客户结算",
  759. "count": pending_settlement_count,
  760. "items": settle_lines,
  761. },
  762. ]
  763. # “客户总数/供应商总数”口径:统计用户表(sys_user)并按 region_id 区分
  764. # - region_id 为空:不过滤全局
  765. # - region_id 传入:按 SysUser.region_id 过滤
  766. customer_count = (
  767. db.query(SysUser)
  768. .join(SysRegion, SysUser.region_id == SysRegion.id)
  769. .filter(
  770. and_(
  771. SysUser.is_del == 0,
  772. SysUser.role == "client",
  773. SysRegion.is_del == 0,
  774. *region_active_clause,
  775. *([SysUser.region_id == region_id] if region_id else []),
  776. )
  777. )
  778. .count()
  779. )
  780. supplier_count = (
  781. db.query(SysUser)
  782. .join(SysRegion, SysUser.region_id == SysRegion.id)
  783. .filter(
  784. and_(
  785. SysUser.is_del == 0,
  786. SysUser.role == "supplier",
  787. SysRegion.is_del == 0,
  788. *region_active_clause,
  789. *([SysUser.region_id == region_id] if region_id else []),
  790. )
  791. )
  792. .count()
  793. )
  794. return {
  795. "todayOrderCount": today_order_count,
  796. "todayOrderAmount": today_order_amount,
  797. "monthOrderCount": len(month_orders),
  798. "monthOrderAmount": round(sum(float(o.total_amount or 0) for o in month_orders), 2),
  799. "pendingDeliveryCount": pending_delivery_count,
  800. "completedOrderCount": sum(
  801. 1 for o in month_orders if (getattr(o, "order_status", None) or o.status) == "completed"
  802. ),
  803. "activePriceListCount": (
  804. db.query(BizPriceList)
  805. .join(SysRegion, BizPriceList.region_id == SysRegion.id)
  806. .filter(
  807. and_(
  808. BizPriceList.is_del == 0,
  809. BizPriceList.status == "active",
  810. SysRegion.is_del == 0,
  811. *region_active_clause,
  812. *([BizPriceList.region_id == region_id] if region_id else []),
  813. )
  814. )
  815. .count()
  816. ),
  817. "customerCount": customer_count,
  818. "supplierCount": supplier_count,
  819. "todayOrderCountHint": _delta_hint(today_order_count, prev_count, " 单"),
  820. "todayOrderAmountHint": _amount_delta_hint(today_order_amount, prev_amount),
  821. "deliveryWarningCount": delivery_warning_count,
  822. "recentOrders": recent_orders,
  823. "todoSnapshots": todo_snapshots,
  824. }
  825. statistics_crud = StatisticsCRUD()