statistics.py 28 KB

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