mini_client.py 51 KB

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