mini_client.py 54 KB

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