mini_client.py 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467
  1. """小程序端客户端接口"""
  2. import re
  3. from typing import Optional
  4. from fastapi import APIRouter, Depends, Request, Query
  5. from sqlalchemy.orm import Session
  6. from sqlalchemy import and_
  7. from app.core.database import get_db
  8. from app.core.auth import get_current_user, get_client_ip
  9. from app.core.response import success_response, error_response
  10. from app.models.models import (
  11. BasAddress,
  12. BasProduct,
  13. BasProductCategory,
  14. BasProductUnit,
  15. BizDeliveryTask,
  16. BizOrderItem,
  17. BizPriceListItem,
  18. SysUser,
  19. )
  20. from app.crud.address import address_crud
  21. from app.crud.user import user_crud
  22. from app.crud.region import region_crud
  23. router = APIRouter(prefix="/api/mini/client", tags=["小程序端-客户端接口"])
  24. def require_client(current_user: dict = Depends(get_current_user)):
  25. """验证客户权限"""
  26. if current_user.get("role") != "client":
  27. return error_response(403, "需要客户权限")
  28. return current_user
  29. def require_supplier_or_client(current_user: dict = Depends(get_current_user)):
  30. """验证供应商或客户权限"""
  31. if current_user.get("role") not in ["client", "supplier"]:
  32. return error_response(403, "需要供应商或客户权限")
  33. return current_user
  34. def _deliverer_contact_phone(du: SysUser) -> Optional[str]:
  35. """配送员联系电话:优先 sys_user.phone;为空则尝试 username 为 11 位手机号的情况。"""
  36. p = (getattr(du, "phone", None) or "").strip()
  37. if p:
  38. return p
  39. un = (getattr(du, "username", None) or "").strip()
  40. if re.fullmatch(r"1[3-9]\d{9}", un):
  41. return un
  42. return None
  43. # ============ 地址管理 ============
  44. def _address_to_client_dict(addr) -> dict:
  45. return {
  46. "id": addr.id,
  47. "userId": addr.user_id,
  48. "addressType": addr.address_type,
  49. "contactPerson": addr.contact_person,
  50. "contactPhone": addr.contact_phone,
  51. "province": addr.province,
  52. "city": addr.city,
  53. "district": addr.district,
  54. "detailAddress": addr.detail_address,
  55. "isDefault": addr.is_default,
  56. "createAt": str(addr.create_at) if addr.create_at else None,
  57. }
  58. @router.get("/addresses", response_model=dict)
  59. def list_addresses(
  60. db: Session = Depends(get_db),
  61. current_user: dict = Depends(require_client),
  62. page: Optional[int] = Query(None, ge=1, description="传则分页;不传则返回全量数组(兼容下单/选地址等)"),
  63. pageSize: Optional[int] = Query(None, ge=1, le=100, description="分页条数,默认 20"),
  64. ):
  65. """获取客户地址列表"""
  66. uid = current_user.get("user_id")
  67. if page is None:
  68. addresses = address_crud.get_by_user(db, uid)
  69. return success_response([_address_to_client_dict(a) for a in addresses])
  70. ps = pageSize if pageSize is not None else 20
  71. skip = (page - 1) * ps
  72. addresses, total = address_crud.get_by_user_paged(db, uid, skip=skip, limit=ps)
  73. items = [_address_to_client_dict(a) for a in addresses]
  74. total_pages = (total + ps - 1) // ps if total else 0
  75. return success_response({
  76. "list": items,
  77. "total": total,
  78. "page": page,
  79. "pageSize": ps,
  80. "totalPages": total_pages,
  81. })
  82. @router.get("/addresses/{address_id}", response_model=dict)
  83. def get_address(
  84. address_id: str,
  85. db: Session = Depends(get_db),
  86. current_user: dict = Depends(require_client)
  87. ):
  88. """获取地址详情"""
  89. address = address_crud.get(db, address_id)
  90. if not address or address.user_id != current_user.get("user_id"):
  91. return error_response(404, "地址不存在")
  92. return success_response({
  93. "id": address.id,
  94. "userId": address.user_id,
  95. "addressType": address.address_type,
  96. "contactPerson": address.contact_person,
  97. "contactPhone": address.contact_phone,
  98. "province": address.province,
  99. "city": address.city,
  100. "district": address.district,
  101. "detailAddress": address.detail_address,
  102. "isDefault": address.is_default,
  103. "createAt": str(address.create_at) if address.create_at else None
  104. })
  105. @router.post("/addresses", response_model=dict)
  106. def create_address(
  107. request: dict,
  108. db: Session = Depends(get_db),
  109. current_user: dict = Depends(require_client)
  110. ):
  111. """创建地址"""
  112. request["user_id"] = current_user.get("user_id")
  113. creator = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  114. # 如果设置为默认,先取消其他默认
  115. if request.get("isDefault"):
  116. address_crud.set_default(db, current_user.get("user_id"), "")
  117. address = address_crud.create(db, request, creator)
  118. return success_response({
  119. "id": address.id,
  120. "addressType": address.address_type,
  121. "contactPerson": address.contact_person,
  122. "contactPhone": address.contact_phone
  123. })
  124. @router.put("/addresses/{address_id}", response_model=dict)
  125. def update_address(
  126. address_id: str,
  127. request: dict,
  128. db: Session = Depends(get_db),
  129. current_user: dict = Depends(require_client)
  130. ):
  131. """更新地址"""
  132. address = address_crud.get(db, address_id)
  133. if not address or address.user_id != current_user.get("user_id"):
  134. return error_response(404, "地址不存在")
  135. # 如果设置为默认,先取消其他默认
  136. if request.get("isDefault"):
  137. address_crud.set_default(db, current_user.get("user_id"), address_id)
  138. updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  139. address = address_crud.update_with_history(db, address_id, request, updater)
  140. return success_response({
  141. "id": address.id,
  142. "addressType": address.address_type,
  143. "contactPerson": address.contact_person,
  144. "contactPhone": address.contact_phone
  145. })
  146. @router.delete("/addresses/{address_id}", response_model=dict)
  147. def delete_address(
  148. address_id: str,
  149. db: Session = Depends(get_db),
  150. current_user: dict = Depends(require_client)
  151. ):
  152. """删除地址"""
  153. address = address_crud.get(db, address_id)
  154. if not address or address.user_id != current_user.get("user_id"):
  155. return error_response(404, "地址不存在")
  156. success = address_crud.soft_delete_with_history(db, address_id)
  157. return success_response({"id": address_id})
  158. @router.post("/addresses/{address_id}/set-default", response_model=dict)
  159. def set_default_address(
  160. address_id: str,
  161. db: Session = Depends(get_db),
  162. current_user: dict = Depends(require_client)
  163. ):
  164. """设置默认地址"""
  165. address = address_crud.get(db, address_id)
  166. if not address or address.user_id != current_user.get("user_id"):
  167. return error_response(404, "地址不存在")
  168. success = address_crud.set_default(db, current_user.get("user_id"), address_id)
  169. if success:
  170. return success_response({"message": "设置成功"})
  171. return error_response(400, "设置失败")
  172. # ============ 个人资料 ============
  173. @router.get("/profile", response_model=dict)
  174. def get_profile(
  175. db: Session = Depends(get_db),
  176. current_user: dict = Depends(require_client)
  177. ):
  178. """获取客户个人资料"""
  179. user = user_crud.get(db, current_user.get("user_id"))
  180. if not user:
  181. return error_response(404, "用户不存在")
  182. region_name = None
  183. if user.region_id:
  184. region = region_crud.get(db, user.region_id)
  185. region_name = region.name if region else None
  186. # 获取默认配送地址
  187. default_address = address_crud.get_default(db, user.id, "delivery")
  188. address_info = None
  189. if default_address:
  190. address_info = {
  191. "contactPerson": default_address.contact_person,
  192. "contactPhone": default_address.contact_phone,
  193. "province": default_address.province,
  194. "city": default_address.city,
  195. "district": default_address.district,
  196. "detailAddress": default_address.detail_address
  197. }
  198. return success_response({
  199. "id": user.id,
  200. "username": user.username,
  201. "realName": user.real_name,
  202. "phone": user.phone,
  203. "role": user.role,
  204. "regionId": user.region_id,
  205. "regionName": region_name,
  206. "address": address_info
  207. })
  208. @router.put("/profile", response_model=dict)
  209. def update_profile(
  210. request: dict,
  211. db: Session = Depends(get_db),
  212. current_user: dict = Depends(require_client)
  213. ):
  214. """更新客户个人资料"""
  215. user = user_crud.get(db, current_user.get("user_id"))
  216. if not user:
  217. return error_response(404, "用户不存在")
  218. updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  219. user = user_crud.update_with_history(db, current_user.get("user_id"), request, updater)
  220. return success_response({
  221. "id": user.id,
  222. "realName": user.real_name,
  223. "phone": user.phone
  224. })
  225. # ============ 价格清单 - 客户端查看 ============
  226. @router.get("/price-list/current", response_model=dict)
  227. def get_current_price_list(
  228. db: Session = Depends(get_db),
  229. current_user: dict = Depends(require_client)
  230. ):
  231. """获取当前生效的价格清单"""
  232. from app.crud.price_list import price_list_crud, price_list_item_crud
  233. user = user_crud.get(db, current_user.get("user_id"))
  234. if not user or not user.region_id:
  235. return error_response(400, "用户未绑定区域")
  236. price_list = price_list_crud.get_active(db, user.region_id)
  237. if not price_list:
  238. return success_response(None)
  239. # 批量拉取价格项 + 商品/分类/单位,避免循环 N+1
  240. items = (
  241. db.query(
  242. BizPriceListItem.product_id,
  243. BizPriceListItem.unit_id,
  244. BizPriceListItem.price,
  245. BasProduct.name.label("product_name"),
  246. BasProduct.image_url.label("image_url"),
  247. BasProduct.category_id.label("category_id"),
  248. BasProductCategory.name.label("category_name"),
  249. BasProductUnit.name.label("unit_name"),
  250. )
  251. .outerjoin(
  252. BasProduct,
  253. and_(BizPriceListItem.product_id == BasProduct.id, BasProduct.is_del == 0),
  254. )
  255. .outerjoin(
  256. BasProductCategory,
  257. and_(BasProduct.category_id == BasProductCategory.id, BasProductCategory.is_del == 0),
  258. )
  259. .outerjoin(
  260. BasProductUnit,
  261. and_(BizPriceListItem.unit_id == BasProductUnit.id, BasProductUnit.is_del == 0),
  262. )
  263. .filter(BizPriceListItem.price_list_id == price_list.id, BizPriceListItem.is_del == 0)
  264. .all()
  265. )
  266. # 获取关联信息
  267. region = region_crud.get(db, price_list.region_id)
  268. region_name = region.name if region else None
  269. supplier = user_crud.get(db, price_list.supplier_id)
  270. supplier_name = supplier.real_name if supplier else None
  271. item_details = [
  272. {
  273. "productId": item.product_id,
  274. "productName": item.product_name,
  275. "categoryName": item.category_name,
  276. "unitId": item.unit_id,
  277. "unitName": item.unit_name,
  278. "price": float(item.price) if item.price else 0,
  279. "imageUrl": item.image_url,
  280. }
  281. for item in items
  282. ]
  283. return success_response({
  284. "id": price_list.id,
  285. "regionId": price_list.region_id,
  286. "regionName": region_name,
  287. "supplierId": price_list.supplier_id,
  288. "supplierName": supplier_name,
  289. "version": price_list.version,
  290. "effectiveTime": str(price_list.effective_time) if price_list.effective_time else None,
  291. "publishedTime": str(price_list.published_time) if price_list.published_time else None,
  292. "items": item_details
  293. })
  294. @router.get("/price-list/products", response_model=dict)
  295. def get_price_list_products(
  296. categoryId: Optional[str] = Query(None, description="分类ID"),
  297. keyword: Optional[str] = Query(None, description="搜索关键词"),
  298. page: int = Query(1, ge=1, description="页码"),
  299. pageSize: int = Query(20, ge=1, le=100, description="每页条数"),
  300. db: Session = Depends(get_db),
  301. current_user: dict = Depends(require_client)
  302. ):
  303. """获取价格清单商品列表(带分类和搜索)"""
  304. from app.crud.price_list import price_list_crud, price_list_item_crud
  305. from app.crud.product import product_crud
  306. from app.crud.category import category_crud
  307. from app.crud.unit import unit_crud
  308. user = user_crud.get(db, current_user.get("user_id"))
  309. if not user or not user.region_id:
  310. return error_response(400, "用户未绑定区域")
  311. price_list = price_list_crud.get_active(db, user.region_id)
  312. if not price_list:
  313. return success_response({
  314. "list": [],
  315. "total": 0,
  316. "page": page,
  317. "pageSize": pageSize,
  318. "totalPages": 0
  319. })
  320. items = price_list_item_crud.get_by_price_list(db, price_list.id)
  321. # 获取商品信息并筛选
  322. result = []
  323. for item in items:
  324. product = product_crud.get(db, item.product_id)
  325. if not product:
  326. continue
  327. # 分类筛选
  328. if categoryId and product.category_id != categoryId:
  329. continue
  330. # 关键词筛选
  331. if keyword:
  332. if keyword.lower() not in (product.name or "").lower():
  333. continue
  334. category_name = None
  335. if product.category_id:
  336. category = category_crud.get(db, product.category_id)
  337. category_name = category.name if category else None
  338. unit_name = None
  339. if item.unit_id:
  340. u = unit_crud.get(db, item.unit_id)
  341. unit_name = u.name if u else None
  342. result.append({
  343. "productId": item.product_id,
  344. "productName": product.name,
  345. "categoryId": product.category_id,
  346. "categoryName": category_name,
  347. "unitId": item.unit_id,
  348. "unitName": unit_name,
  349. "price": float(item.price) if item.price else 0,
  350. "imageUrl": product.image_url,
  351. "description": product.description
  352. })
  353. # 分页
  354. total = len(result)
  355. start = (page - 1) * pageSize
  356. end = start + pageSize
  357. paginated = result[start:end]
  358. return success_response({
  359. "list": paginated,
  360. "total": total,
  361. "page": page,
  362. "pageSize": pageSize,
  363. "totalPages": (total + pageSize - 1) // pageSize
  364. })
  365. # ============ 订单管理 - 客户端 ============
  366. # 订单状态映射
  367. CLIENT_ORDER_STATUS_MAP = {
  368. "pending_payment": "待支付",
  369. "pending_review_cancel": "待审核取消",
  370. "pending_dispatch": "待分配配送",
  371. "delivering": "配送中",
  372. "delivered": "配送完成",
  373. "pending_confirm": "待确认收货",
  374. "completed": "已完结",
  375. "cancelled": "已取消"
  376. }
  377. @router.post("/orders/preview", response_model=dict)
  378. def preview_order(
  379. request: dict,
  380. db: Session = Depends(get_db),
  381. current_user: dict = Depends(require_client)
  382. ):
  383. """订单预览"""
  384. from app.crud.order import order_crud
  385. from app.crud.price_list import price_list_crud, price_list_item_crud
  386. from app.crud.product import product_crud
  387. from app.crud.unit import unit_crud
  388. from decimal import Decimal
  389. price_list_id = request.get("priceListId")
  390. items = request.get("items", [])
  391. address_id = request.get("addressId")
  392. if not price_list_id:
  393. return error_response(400, "请选择价格清单")
  394. if not items:
  395. return error_response(400, "请选择商品")
  396. if not address_id:
  397. return error_response(400, "请选择配送地址")
  398. # 获取价格清单
  399. price_list = price_list_crud.get(db, price_list_id)
  400. if not price_list:
  401. return error_response(404, "价格清单不存在")
  402. # 获取地址信息
  403. address = address_crud.get(db, address_id)
  404. if not address or address.user_id != current_user.get("user_id"):
  405. return error_response(404, "地址不存在")
  406. # 获取价格清单项
  407. price_items = price_list_item_crud.get_by_price_list(db, price_list_id)
  408. price_map = {item.product_id: item for item in price_items}
  409. # 计算金额(使用Decimal确保精度)
  410. preview_items = []
  411. total_amount = Decimal("0")
  412. for item in items:
  413. product_id = item.get("productId")
  414. quantity = item.get("quantity", 0)
  415. price_item = price_map.get(product_id)
  416. if not price_item:
  417. return error_response(400, f"商品 {product_id} 不在价格清单中")
  418. product = product_crud.get(db, product_id)
  419. unit = unit_crud.get(db, price_item.unit_id) if price_item.unit_id else None
  420. # 使用Decimal进行精确计算
  421. item_amount = Decimal(str(quantity)) * Decimal(str(price_item.price))
  422. total_amount += item_amount
  423. preview_items.append({
  424. "productId": product_id,
  425. "productName": product.name if product else None,
  426. "unitName": unit.name if unit else None,
  427. "unitPrice": float(price_item.price),
  428. "quantity": quantity,
  429. "amount": float(item_amount)
  430. })
  431. # 拼接地址
  432. full_address = f"{address.province or ''}{address.city or ''}{address.district or ''}{address.detail_address or ''}"
  433. return success_response({
  434. "priceListId": price_list_id,
  435. "priceVersion": price_list.version,
  436. "items": preview_items,
  437. "totalAmount": float(total_amount),
  438. "address": {
  439. "contactPerson": address.contact_person,
  440. "contactPhone": address.contact_phone,
  441. "fullAddress": full_address
  442. }
  443. })
  444. @router.post("/orders", response_model=dict)
  445. def create_order(
  446. request: dict,
  447. db: Session = Depends(get_db),
  448. current_user: dict = Depends(require_client)
  449. ):
  450. """创建订单"""
  451. from app.crud.order import order_crud
  452. from app.crud.price_list import price_list_crud, price_list_item_crud
  453. price_list_id = request.get("priceListId")
  454. items = request.get("items", [])
  455. address_id = request.get("addressId")
  456. payment_method_raw = request.get("paymentMethod", "online")
  457. payment_method = order_crud.normalize_payment_method(payment_method_raw)
  458. if payment_method not in ["online", "offline"]:
  459. return error_response(400, "支付方式仅支持线上或线下")
  460. remark = request.get("remark")
  461. if not price_list_id:
  462. return error_response(400, "请选择价格清单")
  463. if not items:
  464. return error_response(400, "请选择商品")
  465. if not address_id:
  466. return error_response(400, "请选择配送地址")
  467. # 获取用户信息
  468. user = user_crud.get(db, current_user.get("user_id"))
  469. if not user:
  470. return error_response(404, "用户不存在")
  471. if not user.region_id:
  472. return error_response(400, "用户未绑定区域")
  473. # 获取价格清单
  474. price_list = price_list_crud.get(db, price_list_id)
  475. if not price_list:
  476. return error_response(404, "价格清单不存在")
  477. # 获取地址信息
  478. address = address_crud.get(db, address_id)
  479. if not address or address.user_id != current_user.get("user_id"):
  480. return error_response(404, "地址不存在")
  481. # 获取价格清单项
  482. price_items = price_list_item_crud.get_by_price_list(db, price_list_id)
  483. price_map = {item.product_id: item for item in price_items}
  484. # 验证商品并整理订单项
  485. order_items = []
  486. for item in items:
  487. product_id = item.get("productId")
  488. quantity = item.get("quantity", 0)
  489. unit_id = item.get("unitId")
  490. price_item = price_map.get(product_id)
  491. if not price_item:
  492. return error_response(400, f"商品不在价格清单中")
  493. order_items.append({
  494. "productId": product_id,
  495. "unitId": unit_id or price_item.unit_id,
  496. "quantity": quantity,
  497. "unitPrice": float(price_item.price)
  498. })
  499. # 拼接地址
  500. full_address = f"{address.province or ''}{address.city or ''}{address.district or ''}{address.detail_address or ''}"
  501. creator = {
  502. "user_id": current_user.get("user_id"),
  503. "real_name": current_user.get("real_name")
  504. }
  505. order = order_crud.create_order(
  506. db,
  507. client_id=current_user.get("user_id"),
  508. region_id=user.region_id,
  509. price_list_id=price_list_id,
  510. price_version=price_list.version,
  511. items=order_items,
  512. address_id=address_id,
  513. address_info={
  514. "contactPerson": address.contact_person,
  515. "contactPhone": address.contact_phone,
  516. "fullAddress": full_address
  517. },
  518. payment_method=payment_method,
  519. remark=remark,
  520. creator=creator
  521. )
  522. order_status = getattr(order, "order_status", None) or "pending_confirm"
  523. delivery_status = getattr(order, "delivery_status", None) or "pending_dispatch"
  524. pay_status = getattr(order, "pay_status", None) or "pending_payment"
  525. return success_response({
  526. "orderId": order.id,
  527. "orderNo": order.order_no,
  528. "totalAmount": float(order.total_amount),
  529. "status": order.status,
  530. "orderStatus": order_status,
  531. "deliveryStatus": delivery_status,
  532. "payStatus": pay_status,
  533. "paymentMethod": order_crud.normalize_payment_method(order.payment_method),
  534. })
  535. @router.get("/orders", response_model=dict)
  536. def list_client_orders(
  537. status: Optional[str] = Query(None, description="订单状态(兼容旧参数;与 listTab 同时传时以 listTab 为准)"),
  538. orderStatus: Optional[str] = Query(None, description="订单主状态:pending_payment/pending_confirm/completed/cancelled"),
  539. listTab: Optional[str] = Query(
  540. None,
  541. description="列表分段:all / pending_payment / pending_confirm / completed / cancelled,用于 Tab 独立分页",
  542. ),
  543. page: int = Query(1, ge=1, description="页码"),
  544. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  545. db: Session = Depends(get_db),
  546. current_user: dict = Depends(require_client)
  547. ):
  548. """获取客户订单列表"""
  549. from app.crud.order import order_crud, order_item_crud
  550. segment = (listTab or "all").strip().lower()
  551. if segment not in ("all", "pending_payment", "pending_confirm", "completed", "cancelled"):
  552. segment = "all"
  553. use_status = status if segment == "all" else None
  554. use_list_tab = segment if segment != "all" else None
  555. use_order_status = (orderStatus or "").strip() if segment == "all" else None
  556. orders, total = order_crud.get_multi_with_filters(
  557. db,
  558. client_id=current_user.get("user_id"),
  559. status=use_status,
  560. order_status=use_order_status or None,
  561. list_tab=use_list_tab,
  562. skip=(page - 1) * pageSize,
  563. limit=pageSize
  564. )
  565. order_ids = [o.id for o in orders]
  566. # 批量获取订单项(用于订单列表中展示商品列表)
  567. items_by_order_id: dict[str, list[BizOrderItem]] = {}
  568. product_map: dict[str, BasProduct] = {}
  569. if order_ids:
  570. all_items: list[BizOrderItem] = db.query(BizOrderItem).filter(
  571. BizOrderItem.is_del == 0,
  572. BizOrderItem.order_id.in_(order_ids)
  573. ).all()
  574. for it in all_items:
  575. items_by_order_id.setdefault(it.order_id, []).append(it)
  576. product_ids = list({it.product_id for it in all_items if it.product_id})
  577. if product_ids:
  578. products: list[BasProduct] = db.query(BasProduct).filter(
  579. BasProduct.is_del == 0,
  580. BasProduct.id.in_(product_ids)
  581. ).all()
  582. product_map = {p.id: p for p in products}
  583. result = []
  584. for order in orders:
  585. order_status = getattr(order, "order_status", None) or "pending_confirm"
  586. delivery_status = getattr(order, "delivery_status", None) or "pending_dispatch"
  587. pay_status = getattr(order, "pay_status", None) or "pending_payment"
  588. order_items = items_by_order_id.get(order.id, [])
  589. item_count = len(order_items) if order_items else order_item_crud.get_count_by_order(db, order.id)
  590. total_quantity = float(sum([(it.quantity or 0) for it in order_items])) if order_items else None
  591. item_details = []
  592. for it in order_items:
  593. p = product_map.get(it.product_id)
  594. item_details.append({
  595. "id": it.id,
  596. "productId": it.product_id,
  597. "productName": (p.name if p else None),
  598. "quantity": float(it.quantity) if it.quantity else 0,
  599. "imageUrl": (p.image_url if p else None),
  600. })
  601. result.append({
  602. "id": order.id,
  603. "orderNo": order.order_no,
  604. "totalAmount": float(order.total_amount) if order.total_amount else 0,
  605. "paidAmount": float(order.paid_amount) if order.paid_amount else 0,
  606. "status": order.status,
  607. "orderStatus": order_status,
  608. "deliveryStatus": delivery_status,
  609. "payStatus": pay_status,
  610. "paymentMethod": order_crud.normalize_payment_method(order.payment_method),
  611. "itemCount": item_count,
  612. "totalQuantity": total_quantity if total_quantity is not None else item_count,
  613. "items": item_details,
  614. "createAt": str(order.create_at) if order.create_at else None
  615. })
  616. return success_response({
  617. "list": result,
  618. "total": total,
  619. "page": page,
  620. "pageSize": pageSize,
  621. "totalPages": (total + pageSize - 1) // pageSize
  622. })
  623. @router.get("/orders/{order_id}", response_model=dict)
  624. def get_client_order(
  625. order_id: str,
  626. db: Session = Depends(get_db),
  627. current_user: dict = Depends(require_client)
  628. ):
  629. """获取客户订单详情"""
  630. from app.crud.order import order_crud, order_item_crud, order_status_log_crud
  631. from app.crud.product import product_crud
  632. from app.crud.unit import unit_crud
  633. from app.crud.deliverer_review import deliverer_review_crud
  634. order = order_crud.get(db, order_id)
  635. if not order:
  636. return error_response(404, "订单不存在")
  637. if order.client_id != current_user.get("user_id"):
  638. return error_response(403, "无权查看此订单")
  639. # 获取订单项
  640. items = order_item_crud.get_by_order(db, order_id)
  641. # 配送信息与评价信息
  642. from app.crud.deliverer_review import deliverer_review_crud
  643. delivery_task = db.query(BizDeliveryTask).filter(
  644. BizDeliveryTask.order_id == order_id,
  645. BizDeliveryTask.is_del == 0
  646. ).first()
  647. deliverer_name = None
  648. deliverer_phone = None
  649. if delivery_task and delivery_task.deliver_id:
  650. du = db.query(SysUser).filter(
  651. SysUser.id == delivery_task.deliver_id,
  652. SysUser.is_del == 0,
  653. ).first()
  654. if du:
  655. deliverer_name = du.real_name or du.username
  656. deliverer_phone = _deliverer_contact_phone(du)
  657. existing_review = deliverer_review_crud.get_by_order(db, order_id)
  658. # 获取状态日志
  659. status_logs = order_status_log_crud.get_by_order(db, order_id)
  660. # 获取区域名称
  661. region_name = None
  662. if order.region_id:
  663. region = region_crud.get(db, order.region_id)
  664. region_name = region.name if region else None
  665. # 处理订单项
  666. item_details = []
  667. for item in items:
  668. product = product_crud.get(db, item.product_id)
  669. unit = unit_crud.get(db, item.unit_id) if item.unit_id else None
  670. item_details.append({
  671. "id": item.id,
  672. "productId": item.product_id,
  673. "productName": product.name if product else None,
  674. "unitId": item.unit_id,
  675. "unitName": unit.name if unit else None,
  676. "measureValue": float(product.measure_value) if (product and getattr(product, "measure_value", None) is not None) else None,
  677. "quantity": float(item.quantity) if item.quantity else 0,
  678. "unitPrice": float(item.unit_price) if item.unit_price else 0,
  679. "amount": float(item.amount) if item.amount else 0,
  680. "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0,
  681. "status": item.status,
  682. "imageUrl": product.image_url if product else None,
  683. })
  684. # 处理状态日志
  685. log_details = []
  686. for log in status_logs:
  687. log_details.append({
  688. "status": log.to_status,
  689. "statusText": CLIENT_ORDER_STATUS_MAP.get(log.to_status, log.to_status),
  690. # BizOrderStatusLog 无 operator_name 字段;优先用 create_name(通用审计字段)
  691. "operatorName": getattr(log, "create_name", None) or "",
  692. "operatorRole": log.operator_role,
  693. "reason": log.reason,
  694. "createAt": str(log.create_at) if log.create_at else None
  695. })
  696. from app.core.order_status_compat import derive_three_status_from_legacy
  697. ts = derive_three_status_from_legacy(order.status, paid_amount=order.paid_amount)
  698. order_status = getattr(order, "order_status", None) or ts.order_status
  699. delivery_status = getattr(order, "delivery_status", None) or ts.delivery_status
  700. pay_status = getattr(order, "pay_status", None) or ts.pay_status
  701. return success_response({
  702. "id": order.id,
  703. "orderNo": order.order_no,
  704. "regionId": order.region_id,
  705. "regionName": region_name,
  706. "priceListId": order.price_list_id,
  707. "priceVersion": order.price_version,
  708. "status": order.status,
  709. "orderStatus": order_status,
  710. "deliveryStatus": delivery_status,
  711. "payStatus": pay_status,
  712. "paymentMethod": order_crud.normalize_payment_method(order.payment_method),
  713. "totalAmount": float(order.total_amount) if order.total_amount else 0,
  714. "paidAmount": float(order.paid_amount) if order.paid_amount else 0,
  715. "receivableAmount": float(order.receivable_amount) if order.receivable_amount else 0,
  716. "refundAmount": float(order.refund_amount) if order.refund_amount else 0,
  717. "contactPerson": order.contact_person,
  718. "contactPhone": order.contact_phone,
  719. "deliveryAddress": order.delivery_address,
  720. "remark": order.remark,
  721. "cancelReason": order.cancel_reason,
  722. "createAt": str(order.create_at) if order.create_at else None,
  723. "paymentTime": str(order.payment_time) if order.payment_time else None,
  724. "deliveryAssignTime": str(getattr(order, "delivery_assign_time", None)) if getattr(order, "delivery_assign_time", None) else None,
  725. "deliveredTime": str(getattr(order, "delivered_time", None)) if getattr(order, "delivered_time", None) else None,
  726. "delivererId": delivery_task.deliver_id if delivery_task else None,
  727. "delivererName": deliverer_name,
  728. "delivererPhone": deliverer_phone,
  729. "canReviewDeliverer": order.status == "completed" and delivery_task is not None and not existing_review,
  730. "delivererReview": {
  731. "id": existing_review.id,
  732. "score": existing_review.score,
  733. "tags": (existing_review.tags or "").split(",") if existing_review and existing_review.tags else [],
  734. "content": existing_review.content,
  735. "createAt": str(existing_review.create_at) if existing_review and existing_review.create_at else None
  736. } if existing_review else None,
  737. "items": item_details,
  738. "statusLogs": log_details
  739. })
  740. @router.post("/orders/{order_id}/deliverer-review", response_model=dict)
  741. def review_deliverer(
  742. order_id: str,
  743. request: dict,
  744. db: Session = Depends(get_db),
  745. current_user: dict = Depends(require_client)
  746. ):
  747. """客户评价配送员"""
  748. from app.crud.order import order_crud
  749. from app.crud.deliverer_review import deliverer_review_crud
  750. order = order_crud.get(db, order_id)
  751. if not order:
  752. return error_response(404, "订单不存在")
  753. if order.client_id != current_user.get("user_id"):
  754. return error_response(403, "无权操作此订单")
  755. if order.status != "completed":
  756. return error_response(400, "仅已完成订单可评价")
  757. task = db.query(BizDeliveryTask).filter(
  758. BizDeliveryTask.order_id == order_id,
  759. BizDeliveryTask.is_del == 0
  760. ).first()
  761. if not task or not task.deliver_id:
  762. return error_response(400, "未找到对应配送员")
  763. exists = deliverer_review_crud.get_by_order(db, order_id)
  764. if exists:
  765. return error_response(400, "该订单已评价")
  766. score = int(request.get("score", 0))
  767. if score < 1 or score > 5:
  768. return error_response(400, "评分必须在1-5之间")
  769. tags = request.get("tags") or []
  770. content = request.get("content")
  771. is_anonymous = bool(request.get("isAnonymous", False))
  772. creator = {
  773. "user_id": current_user.get("user_id"),
  774. "real_name": current_user.get("real_name")
  775. }
  776. review = deliverer_review_crud.create_review(
  777. db=db,
  778. order_id=order_id,
  779. task_id=task.id,
  780. client_id=current_user.get("user_id"),
  781. deliverer_id=task.deliver_id,
  782. score=score,
  783. tags=tags,
  784. content=content,
  785. is_anonymous=is_anonymous,
  786. creator=creator
  787. )
  788. return success_response({
  789. "id": review.id,
  790. "score": review.score,
  791. "message": "评价成功"
  792. })
  793. @router.post("/orders/{order_id}/cancel", response_model=dict)
  794. def client_cancel_order(
  795. order_id: str,
  796. request: dict,
  797. db: Session = Depends(get_db),
  798. current_user: dict = Depends(require_client)
  799. ):
  800. """客户发起取消申请 — 产品策略:客户端不可自助取消,请联系客服。"""
  801. return error_response(403, "订单取消请联系客服人员处理")
  802. @router.post("/orders/{order_id}/direct-cancel", response_model=dict)
  803. def client_direct_cancel_order(
  804. order_id: str,
  805. request: Optional[dict] = None,
  806. db: Session = Depends(get_db),
  807. current_user: dict = Depends(require_client)
  808. ):
  809. """客户直接取消未支付订单 — 产品策略:客户端不可自助取消,请联系客服或等待超时自动关闭。"""
  810. return error_response(403, "订单取消请联系客服人员处理")
  811. @router.post("/orders/{order_id}/confirm-receive", response_model=dict)
  812. def client_confirm_receive(
  813. order_id: str,
  814. db: Session = Depends(get_db),
  815. current_user: dict = Depends(require_client)
  816. ):
  817. """客户确认收货"""
  818. from app.crud.order import order_crud
  819. order = order_crud.get(db, order_id)
  820. if not order:
  821. return error_response(404, "订单不存在")
  822. if order.client_id != current_user.get("user_id"):
  823. return error_response(403, "无权操作此订单")
  824. order = order_crud.confirm_receive(
  825. db,
  826. order_id=order_id,
  827. operator_id=current_user.get("user_id")
  828. )
  829. if not order:
  830. return error_response(400, "当前状态不支持确认收货")
  831. return success_response({
  832. "id": order.id,
  833. "status": order.status,
  834. "orderStatus": getattr(order, "order_status", None) or "pending_confirm",
  835. "deliveryStatus": getattr(order, "delivery_status", None) or "pending_dispatch",
  836. "payStatus": getattr(order, "pay_status", None) or "pending_payment",
  837. "message": "确认收货成功"
  838. })
  839. @router.get("/orders/{order_id}/delivery-progress", response_model=dict)
  840. def get_delivery_progress(
  841. order_id: str,
  842. db: Session = Depends(get_db),
  843. current_user: dict = Depends(require_client)
  844. ):
  845. """查看配送进度"""
  846. from app.crud.order import order_crud, order_item_crud, order_status_log_crud
  847. from app.crud.product import product_crud
  848. _dt_title = {
  849. "dt_supplier_stocking": "供应商开始备货",
  850. "dt_supplier_ready": "供应商备货完成(待配送员取货)",
  851. "dt_picking": "配送员取货中",
  852. "dt_delivering": "配送员配送中",
  853. "dt_completed": "配送任务已完成",
  854. "dt_assigned": "配送任务已分配",
  855. "dt_pending": "配送任务待分配",
  856. "dt_cancelled": "配送任务已取消",
  857. }
  858. order = order_crud.get(db, order_id)
  859. if not order:
  860. return error_response(404, "订单不存在")
  861. if order.client_id != current_user.get("user_id"):
  862. return error_response(403, "无权查看此订单")
  863. # 获取订单项
  864. items = order_item_crud.get_by_order(db, order_id)
  865. item_details = []
  866. for item in items:
  867. product = product_crud.get(db, item.product_id)
  868. item_details.append({
  869. "productId": item.product_id,
  870. "productName": product.name if product else None,
  871. "quantity": float(item.quantity) if item.quantity else 0,
  872. "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0,
  873. "status": item.status
  874. })
  875. status_logs = order_status_log_crud.get_by_order(db, order_id)
  876. task_events = []
  877. for log in status_logs:
  878. ts = log.to_status or ""
  879. if not str(ts).startswith("dt_"):
  880. continue
  881. task_events.append({
  882. "title": _dt_title.get(ts, ts),
  883. "remark": log.reason,
  884. "operatorRole": log.operator_role,
  885. "createAt": str(log.create_at) if log.create_at else None,
  886. })
  887. task_events.sort(key=lambda x: x.get("createAt") or "")
  888. return success_response({
  889. "orderId": order.id,
  890. "orderNo": order.order_no,
  891. "status": order.status,
  892. "orderStatus": getattr(order, "order_status", None),
  893. "deliveryStatus": getattr(order, "delivery_status", None),
  894. "payStatus": getattr(order, "pay_status", None),
  895. "items": item_details,
  896. "deliveryTaskEvents": task_events,
  897. })
  898. @router.post("/orders/{order_id}/urge", response_model=dict)
  899. def client_urge_order(
  900. order_id: str,
  901. db: Session = Depends(get_db),
  902. current_user: dict = Depends(require_client)
  903. ):
  904. """客户催单"""
  905. from app.crud.order import order_crud
  906. from app.models.models import BizDeliveryTask
  907. from datetime import datetime, timedelta
  908. order = order_crud.get(db, order_id)
  909. if not order:
  910. return error_response(404, "订单不存在")
  911. if order.client_id != current_user.get("user_id"):
  912. return error_response(403, "无权操作此订单")
  913. # 检查订单状态,只有配送中的订单才能催单
  914. if getattr(order, "delivery_status", None) != "delivering":
  915. return error_response(400, "当前状态不支持催单")
  916. # 获取今天已催单次数(从订单备注中统计)
  917. today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
  918. tomorrow = today + timedelta(days=1)
  919. # 查询今天的催单次数
  920. urge_count = 1 # 默认允许催单
  921. last_urge_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  922. # 获取客户信息
  923. client = user_crud.get(db, current_user.get("user_id"))
  924. client_name = client.real_name if client else "未知客户"
  925. # 获取配送员信息
  926. deliver_name = None
  927. deliver_phone = None
  928. task = db.query(BizDeliveryTask).filter(
  929. BizDeliveryTask.order_id == order_id,
  930. BizDeliveryTask.is_del == 0
  931. ).first()
  932. if task and task.deliver_id:
  933. deliver = user_crud.get(db, task.deliver_id)
  934. if deliver:
  935. deliver_name = deliver.real_name
  936. deliver_phone = deliver.phone
  937. # 发送消息通知配送员
  938. try:
  939. from app.api.message import message_crud
  940. from app.core.utils import generate_id
  941. from app.models.models import BizMessage
  942. # 构造催单内容
  943. urge_content = f"【催单提醒】客户 {client_name} 催单,订单号:{order.order_no},联系电话:{client.phone or '无'}。请尽快处理!"
  944. # 发送给配送员
  945. if task and task.deliver_id:
  946. message_data = {
  947. "id": generate_id(),
  948. "receiver_id": task.deliver_id,
  949. "receiver_name": deliver_name,
  950. "message_type": "delivery",
  951. "title": f"【催单提醒】订单 {order.order_no}",
  952. "content": urge_content,
  953. "is_read": 0,
  954. "create_by": current_user.get("user_id"),
  955. "create_name": client_name
  956. }
  957. message_obj = BizMessage(**message_data)
  958. db.add(message_obj)
  959. # 同时通知运营人员(查找所有admin角色用户)
  960. admin_users = db.query(SysUser).filter(
  961. SysUser.role == "admin",
  962. SysUser.is_active == True,
  963. SysUser.is_del == 0
  964. ).all()
  965. for admin in admin_users:
  966. admin_message_data = {
  967. "id": generate_id(),
  968. "receiver_id": admin.id,
  969. "receiver_name": admin.real_name,
  970. "message_type": "order",
  971. "title": f"【客户催单】订单 {order.order_no}",
  972. "content": urge_content,
  973. "is_read": 0,
  974. "create_by": current_user.get("user_id"),
  975. "create_name": client_name
  976. }
  977. admin_message = BizMessage(**admin_message_data)
  978. db.add(admin_message)
  979. db.commit()
  980. except Exception as e:
  981. # 消息发送失败不影响催单主流程,只记录日志
  982. import logging
  983. logging.warning(f"发送催单消息失败: {str(e)}")
  984. return success_response({
  985. "orderId": order.id,
  986. "urgeCount": urge_count,
  987. "lastUrgeTime": last_urge_time,
  988. "message": "催单成功,配送员将尽快处理"
  989. })
  990. # ============ 结算与退款 - 客户端 ============
  991. @router.get("/settlements", response_model=dict)
  992. def list_client_settlements(
  993. page: int = Query(1, ge=1, description="页码"),
  994. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  995. db: Session = Depends(get_db),
  996. current_user: dict = Depends(require_client)
  997. ):
  998. """获取我的结算记录列表"""
  999. from app.crud.settlement import settlement_crud
  1000. from app.models.models import BizOrder
  1001. client_id = current_user.get("user_id")
  1002. # 获取该客户的所有结算记录
  1003. settlements, total = settlement_crud.get_multi_with_filters(
  1004. db,
  1005. client_id=client_id,
  1006. status="confirmed",
  1007. skip=(page - 1) * pageSize,
  1008. limit=pageSize
  1009. )
  1010. result = []
  1011. for settlement in settlements:
  1012. order = db.query(BizOrder).filter(BizOrder.id == settlement.order_id).first()
  1013. result.append({
  1014. "id": settlement.id,
  1015. "orderId": settlement.order_id,
  1016. "orderNo": order.order_no if order else None,
  1017. "amount": float(settlement.amount) if settlement.amount else 0,
  1018. "settlementMethod": settlement.settlement_method,
  1019. "settlementTime": str(settlement.settlement_time) if settlement.settlement_time else None,
  1020. "settlementNote": settlement.settlement_note,
  1021. "createAt": str(settlement.create_at) if settlement.create_at else None
  1022. })
  1023. return success_response({
  1024. "list": result,
  1025. "total": total,
  1026. "page": page,
  1027. "pageSize": pageSize,
  1028. "totalPages": (total + pageSize - 1) // pageSize
  1029. })
  1030. @router.get("/settlements/{settlement_id}", response_model=dict)
  1031. def get_client_settlement(
  1032. settlement_id: str,
  1033. db: Session = Depends(get_db),
  1034. current_user: dict = Depends(require_client)
  1035. ):
  1036. """获取结算记录详情"""
  1037. from app.crud.settlement import settlement_crud
  1038. from app.models.models import BizOrder
  1039. client_id = current_user.get("user_id")
  1040. settlement = settlement_crud.get(db, settlement_id)
  1041. if not settlement:
  1042. return error_response(404, "结算记录不存在")
  1043. if settlement.client_id != client_id:
  1044. return error_response(403, "无权查看此记录")
  1045. order = db.query(BizOrder).filter(BizOrder.id == settlement.order_id).first()
  1046. return success_response({
  1047. "id": settlement.id,
  1048. "orderId": settlement.order_id,
  1049. "orderNo": order.order_no if order else None,
  1050. "amount": float(settlement.amount) if settlement.amount else 0,
  1051. "settlementMethod": settlement.settlement_method,
  1052. "settlementTime": str(settlement.settlement_time) if settlement.settlement_time else None,
  1053. "settlementNote": settlement.settlement_note,
  1054. "settlementFileUrl": settlement.settlement_file_url,
  1055. "reviewedBy": settlement.reviewed_by,
  1056. "reviewedTime": str(settlement.reviewed_time) if settlement.reviewed_time else None,
  1057. "createAt": str(settlement.create_at) if settlement.create_at else None
  1058. })
  1059. @router.get("/settlements/statistics", response_model=dict)
  1060. def get_client_settlement_statistics(
  1061. db: Session = Depends(get_db),
  1062. current_user: dict = Depends(require_client)
  1063. ):
  1064. """获取我的结算统计"""
  1065. from app.crud.settlement import settlement_crud
  1066. from app.models.models import BizOrder, BizSettlement
  1067. from sqlalchemy import func
  1068. client_id = current_user.get("user_id")
  1069. # 获取已结算总额
  1070. result = db.query(
  1071. func.sum(BizSettlement.amount)
  1072. ).filter(
  1073. BizSettlement.client_id == client_id,
  1074. BizSettlement.status == "confirmed",
  1075. BizSettlement.is_del == 0
  1076. ).scalar() or 0
  1077. return success_response({
  1078. "totalSettled": float(result)
  1079. })
  1080. @router.get("/refunds", response_model=dict)
  1081. def list_client_refunds(
  1082. page: int = Query(1, ge=1, description="页码"),
  1083. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  1084. db: Session = Depends(get_db),
  1085. current_user: dict = Depends(require_client)
  1086. ):
  1087. """获取我的退款记录列表"""
  1088. from app.crud.settlement import refund_crud
  1089. from app.models.models import BizOrder
  1090. client_id = current_user.get("user_id")
  1091. # 获取该客户的所有退款记录(关联订单)
  1092. refunds, total = refund_crud.get_multi_with_filters(
  1093. db,
  1094. skip=(page - 1) * pageSize,
  1095. limit=pageSize
  1096. )
  1097. result = []
  1098. for refund in refunds:
  1099. order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first()
  1100. if order and order.client_id == client_id:
  1101. result.append({
  1102. "id": refund.id,
  1103. "orderId": refund.order_id,
  1104. "orderNo": order.order_no if order else None,
  1105. "refundNo": refund.refund_no,
  1106. "refundAmount": float(refund.refund_amount) if refund.refund_amount else 0,
  1107. "refundMethod": refund.refund_method,
  1108. "refundResult": refund.refund_result,
  1109. "status": refund.status,
  1110. "refundTime": str(refund.refund_time) if refund.refund_time else None,
  1111. "refundFailReason": refund.refund_fail_reason,
  1112. "createAt": str(refund.create_at) if refund.create_at else None
  1113. })
  1114. return success_response({
  1115. "list": result,
  1116. "total": len(result),
  1117. "page": page,
  1118. "pageSize": pageSize,
  1119. "totalPages": (len(result) + pageSize - 1) // pageSize
  1120. })
  1121. @router.get("/refunds/{refund_id}", response_model=dict)
  1122. def get_client_refund(
  1123. refund_id: str,
  1124. db: Session = Depends(get_db),
  1125. current_user: dict = Depends(require_client)
  1126. ):
  1127. """获取退款详情"""
  1128. from app.crud.settlement import refund_crud
  1129. from app.models.models import BizOrder
  1130. client_id = current_user.get("user_id")
  1131. refund = refund_crud.get(db, refund_id)
  1132. if not refund:
  1133. return error_response(404, "退款记录不存在")
  1134. order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first()
  1135. if order and order.client_id != client_id:
  1136. return error_response(403, "无权查看此记录")
  1137. return success_response({
  1138. "id": refund.id,
  1139. "orderId": refund.order_id,
  1140. "orderNo": order.order_no if order else None,
  1141. "refundNo": refund.refund_no,
  1142. "refundAmount": float(refund.refund_amount) if refund.refund_amount else 0,
  1143. "refundMethod": refund.refund_method,
  1144. "refundResult": refund.refund_result,
  1145. "refundTime": str(refund.refund_time) if refund.refund_time else None,
  1146. "refundNoWechat": refund.refund_no_wechat,
  1147. "refundFailReason": refund.refund_fail_reason,
  1148. "status": refund.status,
  1149. "createAt": str(refund.create_at) if refund.create_at else None,
  1150. "createName": refund.create_name
  1151. })
  1152. @router.post("/refunds/apply/{order_id}", response_model=dict)
  1153. def apply_client_refund(
  1154. order_id: str,
  1155. request: dict,
  1156. db: Session = Depends(get_db),
  1157. current_user: dict = Depends(require_client)
  1158. ):
  1159. """申请退款(线下冲销)"""
  1160. from app.crud.settlement import refund_crud
  1161. from app.models.models import BizOrder
  1162. from decimal import Decimal
  1163. client_id = current_user.get("user_id")
  1164. order = db.query(BizOrder).filter(BizOrder.id == order_id).first()
  1165. if not order:
  1166. return error_response(404, "订单不存在")
  1167. if order.client_id != client_id:
  1168. return error_response(403, "无权操作此订单")
  1169. refund_amount = request.get("refundAmount")
  1170. reason = request.get("reason")
  1171. if not refund_amount or refund_amount <= 0:
  1172. return error_response(400, "退款金额必须大于0")
  1173. # 检查已支付金额
  1174. paid_amount = float(order.paid_amount) if order.paid_amount else 0
  1175. if refund_amount > paid_amount:
  1176. return error_response(400, "退款金额不能超过已支付金额")
  1177. # 检查是否有处理中的退款
  1178. existing_refunds = refund_crud.get_by_order(db, order_id)
  1179. processing_refund = any(r.status in ["pending", "processing"] for r in existing_refunds)
  1180. if processing_refund:
  1181. return error_response(400, "该订单有退款正在处理中,请勿重复提交")
  1182. creator = {
  1183. "user_id": current_user.get("user_id"),
  1184. "real_name": current_user.get("real_name")
  1185. }
  1186. refund = refund_crud.create_refund(
  1187. db,
  1188. order_id=order_id,
  1189. refund_amount=Decimal(str(refund_amount)),
  1190. refund_method="offline",
  1191. reason=reason,
  1192. creator=creator
  1193. )
  1194. return success_response({
  1195. "refundId": refund.id,
  1196. "refundNo": refund.refund_no,
  1197. "refundAmount": float(refund.refund_amount),
  1198. "status": refund.status,
  1199. "message": "退款申请已提交"
  1200. })
  1201. @router.get("/refunds/statistics", response_model=dict)
  1202. def get_client_refund_statistics(
  1203. db: Session = Depends(get_db),
  1204. current_user: dict = Depends(require_client)
  1205. ):
  1206. """获取我的退款统计"""
  1207. from app.crud.settlement import refund_crud
  1208. from app.models.models import BizOrder
  1209. from sqlalchemy import func
  1210. client_id = current_user.get("user_id")
  1211. # 获取所有退款记录
  1212. refunds = refund_crud.get_multi_with_filters(db, limit=1000)[0]
  1213. # 筛选属于该客户的
  1214. client_refunds = []
  1215. for refund in refunds:
  1216. order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first()
  1217. if order and order.client_id == client_id:
  1218. client_refunds.append(refund)
  1219. total_refunds = len(client_refunds)
  1220. success_refunds = sum(1 for r in client_refunds if r.status == "completed")
  1221. pending_refunds = sum(1 for r in client_refunds if r.status in ["pending", "processing"])
  1222. failed_refunds = sum(1 for r in client_refunds if r.status == "failed")
  1223. success_amount = sum(
  1224. float(r.refund_amount) for r in client_refunds
  1225. if r.status == "completed" and r.refund_amount
  1226. )
  1227. return success_response({
  1228. "totalRefunds": total_refunds,
  1229. "successRefunds": success_refunds,
  1230. "pendingRefunds": pending_refunds,
  1231. "failedRefunds": failed_refunds,
  1232. "totalRefunded": success_amount
  1233. })