mini_client_extra.py 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199
  1. """小程序客户端扩展接口:购物车、商品分类、仪表盘、发票、下单、支付"""
  2. from typing import Optional, List, Tuple, Dict
  3. from decimal import Decimal
  4. from fastapi import APIRouter, Depends, 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
  9. from app.core.response import success_response, error_response
  10. from app.core.request_payload import resolve_request_payload
  11. from app.core.utils import generate_id
  12. from datetime import datetime
  13. import random
  14. from app.models.models import BizInvoice, BizWechatPay, BizPriceListItem, BasProductUnit
  15. from app.crud.user import user_crud
  16. from app.crud.address import address_crud
  17. from app.crud.client_cart import client_cart_crud
  18. from app.crud.price_list import price_list_crud, price_list_item_crud
  19. from app.crud.product import product_crud
  20. from app.models.models import BasProduct, BasProductCategory
  21. from app.crud.order import order_crud
  22. from app.crud.category import category_crud
  23. from app.crud.unit import unit_crud
  24. from app.crud.product_banner import product_banner
  25. from app.crud.product_detail_media import product_detail_media
  26. from app.crud.product_parameter_value import product_parameter_value
  27. from app.api.mini_client import require_client
  28. router = APIRouter(prefix="/api/mini/client", tags=["小程序端-客户端扩展"])
  29. def _resolve_request_payload(request_body: dict, current_user: dict) -> dict:
  30. return resolve_request_payload(request_body, current_user, source="mini_client_extra")
  31. def _upsert_wechat_pay_pending(
  32. db: Session,
  33. *,
  34. order_id: str,
  35. out_trade_no: str,
  36. total_fee: int,
  37. payer_openid: Optional[str] = None,
  38. operator_id: Optional[str],
  39. operator_name: Optional[str],
  40. ) -> BizWechatPay:
  41. """创建支付单时落一条待支付微信流水(按 out_trade_no 幂等)。"""
  42. row = (
  43. db.query(BizWechatPay)
  44. .filter(BizWechatPay.out_trade_no == out_trade_no, BizWechatPay.is_del == 0)
  45. .first()
  46. )
  47. if row:
  48. row.order_id = order_id
  49. row.total_fee = total_fee
  50. if payer_openid:
  51. row.payer_openid = payer_openid
  52. row.trade_state = row.trade_state or "NOTPAY"
  53. row.trade_state_desc = row.trade_state_desc or "统一下单成功,待支付"
  54. row.update_at = datetime.now()
  55. row.update_by = operator_id
  56. return row
  57. row = BizWechatPay(
  58. id=generate_id(),
  59. order_id=order_id,
  60. out_trade_no=out_trade_no,
  61. payer_openid=payer_openid,
  62. total_fee=total_fee,
  63. trade_state="NOTPAY",
  64. trade_state_desc="统一下单成功,待支付",
  65. create_by=operator_id,
  66. create_name=operator_name,
  67. update_by=operator_id,
  68. )
  69. db.add(row)
  70. return row
  71. def _cart_item_to_dict(
  72. row,
  73. product: Optional[BasProduct] = None,
  74. root_category: Optional[BasProductCategory] = None,
  75. ) -> dict:
  76. return {
  77. "id": row.id,
  78. "productId": row.product_id,
  79. "productName": row.product_name or "",
  80. "categoryId": product.category_id if product and product.category_id else None,
  81. "firstCategoryId": root_category.id if root_category else None,
  82. "firstCategoryName": (root_category.name if root_category else None) or "",
  83. "categoryName": row.category_name or "",
  84. "unitName": row.unit_name or "",
  85. "measureValue": float(product.measure_value) if (product and product.measure_value is not None) else None,
  86. "spec": getattr(product, "spec", None) if product else None,
  87. "price": float(row.unit_price) if row.unit_price is not None else 0,
  88. "quantity": float(row.quantity) if row.quantity is not None else 0,
  89. "amount": float(row.amount) if row.amount is not None else 0,
  90. "imageUrl": row.image_url,
  91. }
  92. def _root_and_leaf_category(
  93. db: Session, category_id: Optional[str], cache: Dict[str, Tuple[Optional[BasProductCategory], Optional[BasProductCategory]]]
  94. ) -> Tuple[Optional[BasProductCategory], Optional[BasProductCategory]]:
  95. """从叶子分类 id 向上解析(含缓存):返回 (根分类, 叶子分类)。"""
  96. if not category_id:
  97. return None, None
  98. if category_id in cache:
  99. return cache[category_id]
  100. leaf = category_crud.get(db, category_id)
  101. if not leaf or getattr(leaf, "is_del", 0):
  102. cache[category_id] = (None, None)
  103. return None, None
  104. cur = leaf
  105. guard = 0
  106. while cur.parent_id and guard < 10:
  107. parent = category_crud.get(db, cur.parent_id)
  108. if not parent or getattr(parent, "is_del", 0):
  109. break
  110. cur = parent
  111. guard += 1
  112. root = cur
  113. cache[category_id] = (root, leaf)
  114. return root, leaf
  115. def _cart_resolve_root_category(db: Session, product: Optional[BasProduct]) -> Optional[BasProductCategory]:
  116. """商品对应的一级(根)分类,供购物车行展示与序列化。"""
  117. if not product or not product.category_id:
  118. return None
  119. cache: Dict[str, Tuple[Optional[BasProductCategory], Optional[BasProductCategory]]] = {}
  120. root, _ = _root_and_leaf_category(db, product.category_id, cache)
  121. return root
  122. def _cart_row_sort_key(
  123. db: Session,
  124. row,
  125. pmap: dict,
  126. cat_cache: Dict[str, Tuple[Optional[BasProductCategory], Optional[BasProductCategory]]],
  127. ) -> tuple:
  128. """购物车:先一级分类 sort_order,再商品名、商品 id(稳定)。"""
  129. product = pmap.get(row.product_id)
  130. pname = (row.product_name or (product.name if product else "") or "").lower()
  131. pid = row.product_id or ""
  132. if not product or not product.category_id:
  133. return (999999, pname, pid)
  134. root, _leaf = _root_and_leaf_category(db, product.category_id, cat_cache)
  135. fo = int(root.sort_order) if root else 999999
  136. return (fo, pname, pid)
  137. def _cart_rows_after_sync(db: Session, uid: str, region_id: Optional[str], op: dict) -> List:
  138. """当前用户有效购物车行(含区域价目表同步、过滤已删)。"""
  139. rows = client_cart_crud.list_by_client(db, uid)
  140. if region_id:
  141. client_cart_crud.sync_to_active_price_list(db, uid, region_id, op, rows=rows)
  142. rows = [r for r in rows if getattr(r, "is_del", 0) == 0]
  143. return rows
  144. def _cart_list_dicts(db: Session, rows: List) -> List[dict]:
  145. """将购物车 ORM 行序列化为与 GET /cart 一致的 list 项(仅未删除行)。"""
  146. rows = [r for r in rows if getattr(r, "is_del", 0) == 0]
  147. product_ids = list({r.product_id for r in rows if r and getattr(r, "product_id", None)})
  148. products = (
  149. db.query(BasProduct)
  150. .filter(and_(BasProduct.id.in_(product_ids), BasProduct.is_del == 0))
  151. .all()
  152. if product_ids
  153. else []
  154. )
  155. pmap = {p.id: p for p in products}
  156. cat_cache: Dict[str, Tuple[Optional[BasProductCategory], Optional[BasProductCategory]]] = {}
  157. rows_sorted = sorted(rows, key=lambda r: _cart_row_sort_key(db, r, pmap, cat_cache))
  158. out: List[dict] = []
  159. for r in rows_sorted:
  160. p = pmap.get(r.product_id)
  161. root: Optional[BasProductCategory] = None
  162. if p and p.category_id:
  163. root, _ = _root_and_leaf_category(db, p.category_id, cat_cache)
  164. out.append(_cart_item_to_dict(r, p, root))
  165. return out
  166. @router.get("/cart/summary", response_model=dict)
  167. def cart_summary(
  168. db: Session = Depends(get_db),
  169. current_user: dict = Depends(require_client),
  170. ):
  171. """购物车汇总(件数、行数、金额),供 Tab 角标等轻量刷新。"""
  172. uid = current_user.get("user_id")
  173. region_id = current_user.get("region_id")
  174. op = {"user_id": uid, "real_name": current_user.get("real_name")}
  175. rows = _cart_rows_after_sync(db, uid, region_id, op)
  176. tq = sum(float(r.quantity or 0) for r in rows)
  177. ta = sum(float(r.amount or 0) for r in rows)
  178. return success_response({
  179. "lineCount": len(rows),
  180. "totalQuantity": tq,
  181. "totalAmount": ta,
  182. })
  183. @router.get("/cart", response_model=dict)
  184. def list_cart(
  185. db: Session = Depends(get_db),
  186. current_user: dict = Depends(require_client),
  187. page: Optional[int] = Query(None, ge=1, description="传则分页;不传则返回全量 list(兼容确认订单等)"),
  188. pageSize: Optional[int] = Query(None, ge=1, le=100, description="分页时每页条数,默认 20"),
  189. ):
  190. uid = current_user.get("user_id")
  191. region_id = current_user.get("region_id")
  192. op = {"user_id": uid, "real_name": current_user.get("real_name")}
  193. rows = _cart_rows_after_sync(db, uid, region_id, op)
  194. full_list = _cart_list_dicts(db, rows)
  195. if page is None:
  196. return success_response({"list": full_list})
  197. ps = pageSize if pageSize is not None else 20
  198. total = len(full_list)
  199. start = (page - 1) * ps
  200. chunk = full_list[start : start + ps]
  201. total_pages = (total + ps - 1) // ps if total else 0
  202. return success_response({
  203. "list": chunk,
  204. "total": total,
  205. "page": page,
  206. "pageSize": ps,
  207. "totalPages": total_pages,
  208. })
  209. @router.post("/cart", response_model=dict)
  210. def add_cart(
  211. request: dict,
  212. db: Session = Depends(get_db),
  213. current_user: dict = Depends(require_client),
  214. ):
  215. payload = _resolve_request_payload(request, current_user)
  216. product_id = payload.get("productId")
  217. quantity = Decimal(str(payload.get("quantity", 1)))
  218. if not product_id or quantity <= 0:
  219. return error_response(400, "商品与数量无效")
  220. region_id = current_user.get("region_id")
  221. if not region_id:
  222. return error_response(400, "用户未绑定区域")
  223. pl = price_list_crud.get_active(db, region_id)
  224. if not pl:
  225. return error_response(404, "当前区域无生效价格清单")
  226. # 只查询该商品对应的价目表行,避免拉取整张价目表明细导致加购变慢
  227. pi = price_list_item_crud.get_by_product_and_price_list(db, product_id, pl.id)
  228. if not pi:
  229. return error_response(400, "商品不在当前价格清单中")
  230. # 用一次 JOIN 拉齐商品 + 单位 + 分类,避免 3 次单查
  231. from app.models.models import BasProductUnit, BasProductCategory
  232. q = (
  233. db.query(BasProduct, BasProductUnit.name, BasProductCategory.name)
  234. .select_from(BasProduct)
  235. # 明确从 BasProduct 出发,避免 join 路径歧义
  236. .outerjoin(BasProductUnit, BasProductUnit.id == pi.unit_id)
  237. .outerjoin(BasProductCategory, BasProductCategory.id == BasProduct.category_id)
  238. .filter(and_(BasProduct.id == product_id, BasProduct.is_del == 0))
  239. )
  240. r = q.first()
  241. product = r[0] if r else None
  242. unit_name = r[1] if r else ""
  243. cat_name = r[2] if r else ""
  244. op = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  245. row = client_cart_crud.add_or_update(
  246. db,
  247. client_id=current_user.get("user_id"),
  248. price_list_id=pl.id,
  249. product_id=product_id,
  250. unit_id=pi.unit_id,
  251. quantity=quantity,
  252. unit_price=Decimal(str(pi.price)),
  253. product_name=product.name if product else "",
  254. unit_name=unit_name or "",
  255. category_name=cat_name,
  256. image_url=product.image_url if product else "",
  257. operator=op,
  258. )
  259. root_cat = _cart_resolve_root_category(db, product)
  260. return success_response(_cart_item_to_dict(row, product, root_cat))
  261. @router.put("/cart/{cart_id}", response_model=dict)
  262. def put_cart(
  263. cart_id: str,
  264. request: dict,
  265. db: Session = Depends(get_db),
  266. current_user: dict = Depends(require_client),
  267. ):
  268. payload = _resolve_request_payload(request, current_user)
  269. qty = Decimal(str(payload.get("quantity", 0)))
  270. if qty <= 0:
  271. return error_response(400, "数量无效")
  272. uid = current_user.get("user_id")
  273. region_id = current_user.get("region_id")
  274. op = {"user_id": uid, "real_name": current_user.get("real_name")}
  275. # 先拿到该行,只有当其 price_list_id 不是当前生效清单时才触发“全车同步”
  276. cur = client_cart_crud.get(db, cart_id)
  277. if not cur or cur.client_id != uid:
  278. return error_response(404, "购物车项不存在")
  279. need_full_cart = False
  280. if region_id:
  281. pl = price_list_crud.get_active(db, region_id)
  282. if pl and getattr(cur, "price_list_id", None) != pl.id:
  283. client_cart_crud.sync_to_active_price_list(db, uid, region_id, op)
  284. # 同步可能合并/软删多行,前端需拉全量列表
  285. need_full_cart = True
  286. row = client_cart_crud.update_quantity(db, cart_id, uid, qty, op, current_row=cur)
  287. if not row:
  288. return error_response(404, "购物车项不存在")
  289. # 行更新接口优先返回轻量字段,避免每次点加减都额外查询商品/分类。
  290. line = _cart_item_to_dict(row, None, None)
  291. if need_full_cart:
  292. # 与 GET /cart 同结构,避免前端再发一次全量请求
  293. rows_after = _cart_rows_after_sync(db, uid, region_id, op)
  294. return success_response({"list": _cart_list_dicts(db, rows_after), "item": line})
  295. return success_response(line)
  296. @router.delete("/cart", response_model=dict)
  297. def delete_cart_clear(
  298. db: Session = Depends(get_db),
  299. current_user: dict = Depends(require_client),
  300. ):
  301. op = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  302. n = client_cart_crud.clear(db, current_user.get("user_id"), op)
  303. return success_response({"cleared": n})
  304. @router.delete("/cart/batch", response_model=dict)
  305. def delete_cart_batch(
  306. request: dict,
  307. db: Session = Depends(get_db),
  308. current_user: dict = Depends(require_client),
  309. ):
  310. payload = _resolve_request_payload(request, current_user)
  311. ids = payload.get("cartIds") or []
  312. if not isinstance(ids, list):
  313. return error_response(400, "cartIds 格式错误")
  314. op = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  315. n = client_cart_crud.batch_delete(db, current_user.get("user_id"), ids, op)
  316. return success_response({"removed": n})
  317. @router.delete("/cart/{cart_id}", response_model=dict)
  318. def delete_cart_one(
  319. cart_id: str,
  320. db: Session = Depends(get_db),
  321. current_user: dict = Depends(require_client),
  322. ):
  323. op = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  324. ok = client_cart_crud.soft_delete(db, cart_id, current_user.get("user_id"), op)
  325. if not ok:
  326. return error_response(404, "购物车项不存在")
  327. return success_response({"ok": True})
  328. @router.get("/price-list", response_model=dict)
  329. def get_price_list_alias(
  330. db: Session = Depends(get_db),
  331. current_user: dict = Depends(require_client),
  332. ):
  333. """与 /price-list/current 相同,便于小程序路径统一"""
  334. user = user_crud.get(db, current_user.get("user_id"))
  335. if not user or not user.region_id:
  336. return error_response(400, "用户未绑定区域")
  337. price_list = price_list_crud.get_active(db, user.region_id)
  338. if not price_list:
  339. return error_response(404, "暂无生效价格清单")
  340. items = price_list_item_crud.get_by_price_list(db, price_list.id)
  341. from app.utils.batch_lookup import prefetch_products_categories_units
  342. product_ids = [str(item.product_id) for item in items if item.product_id]
  343. extra_unit_ids = [str(item.unit_id) for item in items if item.unit_id]
  344. product_map, _cat_map, unit_map = prefetch_products_categories_units(
  345. db, product_ids, extra_unit_ids
  346. )
  347. item_details = []
  348. for item in items:
  349. product = product_map.get(str(item.product_id or ""))
  350. unit_name = unit_map.get(str(item.unit_id)) if item.unit_id else None
  351. item_details.append({
  352. "productId": item.product_id,
  353. "productName": product.name if product else None,
  354. "unitId": item.unit_id,
  355. "unitName": unit_name,
  356. "price": float(item.price) if item.price else 0,
  357. "imageUrl": product.image_url if product else None,
  358. })
  359. return success_response({
  360. "id": price_list.id,
  361. "regionId": price_list.region_id,
  362. "version": price_list.version,
  363. "items": item_details,
  364. })
  365. @router.get("/price-list/active-version", response_model=dict)
  366. def get_active_price_list_version(
  367. db: Session = Depends(get_db),
  368. current_user: dict = Depends(require_client),
  369. ):
  370. """轻量接口:获取当前区域生效价格清单的版本信息(用于客户端兜底校验/去重刷新)。"""
  371. user = user_crud.get(db, current_user.get("user_id"))
  372. if not user or not user.region_id:
  373. return error_response(400, "用户未绑定区域")
  374. pl = price_list_crud.get_active(db, user.region_id)
  375. if not pl:
  376. return success_response(None)
  377. return success_response({
  378. "priceListId": pl.id,
  379. "priceVersion": getattr(pl, "version", 0) or 0,
  380. "publishedTime": str(getattr(pl, "published_time", None)) if getattr(pl, "published_time", None) else None,
  381. })
  382. def _sort_category_dicts_locally(cats: List[dict]) -> List[dict]:
  383. def _key(c: dict):
  384. return (
  385. int(c.get("sortOrder") or 0),
  386. (c.get("name") or "").lower(),
  387. c.get("id") or "",
  388. )
  389. return sorted(cats, key=_key)
  390. def _client_aggregated_categories(db: Session, current_user: dict) -> Tuple[Optional[List[dict]], Optional[str]]:
  391. """返回 (分类 dict 列表, 错误信息)。无清单时返回 ([], None)。"""
  392. user = user_crud.get(db, current_user.get("user_id"))
  393. if not user or not user.region_id:
  394. return None, "用户未绑定区域"
  395. pl = price_list_crud.get_active(db, user.region_id)
  396. if not pl:
  397. return [], None
  398. items = price_list_item_crud.get_by_price_list(db, pl.id)
  399. pmap: Dict[str, dict] = {}
  400. for it in items:
  401. product = product_crud.get(db, it.product_id)
  402. if not product or not product.category_id:
  403. continue
  404. cid = product.category_id
  405. if cid not in pmap:
  406. cat = category_crud.get(db, cid)
  407. pmap[cid] = {
  408. "id": cid,
  409. "name": cat.name if cat else "",
  410. "parentId": cat.parent_id if cat else None,
  411. "sortOrder": cat.sort_order if cat else 0,
  412. "isActive": True,
  413. "imageUrl": cat.image_url if cat else None,
  414. }
  415. return list(pmap.values()), None
  416. @router.get("/product-categories", response_model=dict)
  417. def list_product_categories(
  418. db: Session = Depends(get_db),
  419. current_user: dict = Depends(require_client),
  420. ):
  421. """基于当前生效价格清单中的商品,聚合分类(全量,兼容旧调用)"""
  422. cats, err = _client_aggregated_categories(db, current_user)
  423. if err:
  424. return error_response(400, err)
  425. return success_response(cats)
  426. @router.get("/product-categories/first-level", response_model=dict)
  427. def list_product_categories_first_level(
  428. db: Session = Depends(get_db),
  429. current_user: dict = Depends(require_client),
  430. page: int = Query(1, ge=1),
  431. pageSize: int = Query(8, ge=1, le=50),
  432. ):
  433. """首页一级分类(swiper),服务端分页"""
  434. cats, err = _client_aggregated_categories(db, current_user)
  435. if err:
  436. return error_response(400, err)
  437. sorted_all = _sort_category_dicts_locally(cats or [])
  438. roots = [c for c in sorted_all if not c.get("parentId")]
  439. total = len(roots)
  440. start = (page - 1) * pageSize
  441. slice_roots = roots[start : start + pageSize]
  442. out = [
  443. {
  444. "id": c["id"],
  445. "name": c["name"],
  446. "imageUrl": c.get("imageUrl") or "",
  447. }
  448. for c in slice_roots
  449. ]
  450. total_pages = (total + pageSize - 1) // pageSize if total else 0
  451. return success_response({
  452. "list": out,
  453. "total": total,
  454. "page": page,
  455. "pageSize": pageSize,
  456. "totalPages": total_pages,
  457. })
  458. @router.get("/product-categories/sidebar", response_model=dict)
  459. def list_product_categories_sidebar(
  460. db: Session = Depends(get_db),
  461. current_user: dict = Depends(require_client),
  462. firstCategoryId: str = Query("all", description="一级分类 id,all 表示顶部选「全部」"),
  463. page: int = Query(1, ge=1),
  464. pageSize: int = Query(30, ge=1, le=100),
  465. ):
  466. """首页左侧二级分类栏选项,服务端分页(含「全部」行)"""
  467. cats, err = _client_aggregated_categories(db, current_user)
  468. if err:
  469. return error_response(400, err)
  470. sorted_all = _sort_category_dicts_locally(cats or [])
  471. roots = [c for c in sorted_all if not c.get("parentId")]
  472. rows: List[dict] = [{"id": "all", "name": "全部"}]
  473. fid = (firstCategoryId or "all").strip()
  474. if fid == "all":
  475. rows.extend({"id": c["id"], "name": c["name"]} for c in roots)
  476. else:
  477. children = [c for c in sorted_all if c.get("parentId") == fid]
  478. rows.extend({"id": c["id"], "name": c["name"]} for c in children)
  479. total = len(rows)
  480. start = (page - 1) * pageSize
  481. chunk = rows[start : start + pageSize]
  482. total_pages = (total + pageSize - 1) // pageSize if total else 0
  483. return success_response({
  484. "list": chunk,
  485. "total": total,
  486. "page": page,
  487. "pageSize": pageSize,
  488. "totalPages": total_pages,
  489. })
  490. def _root_category_id_for_leaf(db: Session, category_id: Optional[str], memo: Dict[str, Optional[str]]) -> Optional[str]:
  491. """商品分类叶子节点向上解析一级(根)分类 id,与小程序端 getFirstCategoryId 一致。"""
  492. if not category_id:
  493. return None
  494. if category_id in memo:
  495. return memo[category_id]
  496. cur: Optional[str] = category_id
  497. chain: List[str] = []
  498. while cur and len(chain) < 32:
  499. chain.append(cur)
  500. cat = category_crud.get(db, cur)
  501. if not cat:
  502. root = chain[-1]
  503. for cid in chain:
  504. memo[cid] = root
  505. return root
  506. if not cat.parent_id:
  507. root = cat.id
  508. for cid in chain:
  509. memo[cid] = root
  510. return root
  511. cur = cat.parent_id
  512. root = chain[-1] if chain else None
  513. for cid in chain:
  514. memo[cid] = root
  515. return root
  516. @router.get("/products", response_model=dict)
  517. def list_products(
  518. db: Session = Depends(get_db),
  519. current_user: dict = Depends(require_client),
  520. page: int = Query(1, ge=1),
  521. pageSize: int = Query(20, ge=1, le=100),
  522. categoryId: Optional[str] = None,
  523. rootCategoryId: Optional[str] = None,
  524. keyword: Optional[str] = None,
  525. ):
  526. """分页查询当前价目表中的商品(与 price-list/products 一致逻辑)"""
  527. user = user_crud.get(db, current_user.get("user_id"))
  528. if not user or not user.region_id:
  529. return error_response(400, "用户未绑定区域")
  530. pl = price_list_crud.get_active(db, user.region_id)
  531. if not pl:
  532. return success_response({"list": [], "total": 0, "page": page, "pageSize": pageSize, "totalPages": 0})
  533. item_rows = (
  534. db.query(
  535. BizPriceListItem.product_id,
  536. BizPriceListItem.unit_id,
  537. BizPriceListItem.price,
  538. BasProduct.id.label("pid"),
  539. BasProduct.name.label("product_name"),
  540. BasProduct.category_id.label("category_id"),
  541. BasProduct.measure_value.label("measure_value"),
  542. BasProduct.spec.label("spec"),
  543. BasProduct.description.label("description"),
  544. BasProduct.image_url.label("image_url"),
  545. BasProductUnit.name.label("unit_name"),
  546. )
  547. .join(
  548. BasProduct,
  549. and_(BizPriceListItem.product_id == BasProduct.id, BasProduct.is_del == 0),
  550. )
  551. .outerjoin(
  552. BasProductUnit,
  553. and_(BizPriceListItem.unit_id == BasProductUnit.id, BasProductUnit.is_del == 0),
  554. )
  555. .filter(BizPriceListItem.price_list_id == pl.id, BizPriceListItem.is_del == 0)
  556. .all()
  557. )
  558. category_ids = {r.category_id for r in item_rows if getattr(r, "category_id", None)}
  559. cat_rows = (
  560. db.query(BasProductCategory)
  561. .filter(BasProductCategory.id.in_(category_ids), BasProductCategory.is_del == 0)
  562. .all()
  563. if category_ids
  564. else []
  565. )
  566. cat_cache: Dict[str, Optional[BasProductCategory]] = {c.id: c for c in cat_rows}
  567. # 补齐父链缓存,避免排序/根分类判断时再次打 DB
  568. pending = [c for c in cat_rows if getattr(c, "parent_id", None)]
  569. while pending:
  570. parent_ids = {c.parent_id for c in pending if c.parent_id and c.parent_id not in cat_cache}
  571. if not parent_ids:
  572. break
  573. parents = (
  574. db.query(BasProductCategory)
  575. .filter(BasProductCategory.id.in_(parent_ids), BasProductCategory.is_del == 0)
  576. .all()
  577. )
  578. if not parents:
  579. break
  580. for p in parents:
  581. cat_cache[p.id] = p
  582. pending = [p for p in parents if getattr(p, "parent_id", None)]
  583. def _root_category_id_from_cache(cid: Optional[str]) -> Optional[str]:
  584. if not cid:
  585. return None
  586. cur = cid
  587. seen = set()
  588. for _ in range(32):
  589. if not cur or cur in seen:
  590. break
  591. seen.add(cur)
  592. c = cat_cache.get(cur)
  593. if not c:
  594. return cur
  595. if not getattr(c, "parent_id", None):
  596. return c.id
  597. cur = c.parent_id
  598. return cur
  599. rows = []
  600. kw = keyword.lower() if keyword else None
  601. for item in item_rows:
  602. if categoryId and item.category_id != categoryId:
  603. continue
  604. if rootCategoryId and _root_category_id_from_cache(item.category_id) != rootCategoryId:
  605. continue
  606. if kw and kw not in (item.product_name or "").lower():
  607. continue
  608. cat = cat_cache.get(item.category_id) if item.category_id else None
  609. rows.append({
  610. "id": item.pid,
  611. "name": item.product_name,
  612. "categoryId": item.category_id,
  613. "categoryName": cat.name if cat else "",
  614. "unitId": item.unit_id,
  615. "unitName": item.unit_name or "",
  616. "measureValue": float(item.measure_value) if item.measure_value is not None else None,
  617. "spec": item.spec or None,
  618. "description": item.description,
  619. "imageUrl": item.image_url,
  620. "price": float(item.price) if item.price else 0,
  621. "isActive": True,
  622. "sortOrder": 0,
  623. })
  624. def _sort_tuple(r: dict) -> Tuple:
  625. cid = r.get("categoryId")
  626. first_ord = 0
  627. second_ord = 0
  628. if cid:
  629. chain: List[BasProductCategory] = []
  630. cur: Optional[str] = cid
  631. for _ in range(32):
  632. c = cat_cache.get(cur) if cur else None
  633. if not c:
  634. break
  635. chain.append(c)
  636. if not c.parent_id:
  637. break
  638. cur = c.parent_id
  639. if chain:
  640. second_ord = int(chain[0].sort_order or 0)
  641. first_ord = int(chain[-1].sort_order or 0)
  642. name = (r.get("name") or "").lower()
  643. sid = r.get("id") or ""
  644. return (first_ord, second_ord, int(r.get("sortOrder") or 0), name, sid)
  645. rows.sort(key=_sort_tuple)
  646. total = len(rows)
  647. start = (page - 1) * pageSize
  648. paginated = rows[start : start + pageSize]
  649. total_pages = (total + pageSize - 1) // pageSize if total else 0
  650. return success_response({
  651. "list": paginated,
  652. "total": total,
  653. "page": page,
  654. "pageSize": pageSize,
  655. "totalPages": total_pages,
  656. })
  657. @router.get("/products/{product_id}", response_model=dict)
  658. def get_client_product_detail(
  659. product_id: str,
  660. db: Session = Depends(get_db),
  661. current_user: dict = Depends(require_client),
  662. ):
  663. """客户端商品详情:价目表内商品 + 轮播图 + 参数值 + 富文本 + 详情媒体(图/视频)"""
  664. user = user_crud.get(db, current_user.get("user_id"))
  665. if not user or not user.region_id:
  666. return error_response(400, "用户未绑定区域")
  667. pl = price_list_crud.get_active(db, user.region_id)
  668. if not pl:
  669. return error_response(404, "暂无生效价格清单")
  670. items = price_list_item_crud.get_by_price_list(db, pl.id)
  671. line = next((it for it in items if it.product_id == product_id), None)
  672. if not line:
  673. return error_response(404, "商品不存在或不在当前价目表")
  674. product = product_crud.get(db, product_id)
  675. if not product or getattr(product, "is_del", 0) == 1:
  676. return error_response(404, "商品不存在")
  677. unit = unit_crud.get(db, line.unit_id) if line.unit_id else None
  678. cat_name = ""
  679. if product.category_id:
  680. cat = category_crud.get(db, product.category_id)
  681. cat_name = cat.name if cat else ""
  682. banners_raw = product_banner.get_by_product(db, product_id)
  683. banners = [{
  684. "mediaType": b.media_type,
  685. "fileUrl": b.file_url,
  686. "thumbnailUrl": b.thumbnail_url,
  687. "sortOrder": b.sort_order,
  688. } for b in banners_raw]
  689. pv_rows = product_parameter_value.get_by_product_with_detail(db, product_id)
  690. parameter_values = []
  691. for row in pv_rows:
  692. val = row.get("param_value")
  693. if val is None or str(val).strip() == "":
  694. continue
  695. parameter_values.append({
  696. "parameterId": row.get("parameter_id"),
  697. "parameterName": row.get("parameter_name") or "",
  698. "paramValue": str(val),
  699. })
  700. media_raw = product_detail_media.get_by_product(db, product_id)
  701. detail_media = []
  702. for m in media_raw:
  703. if m.content_type not in ("image", "video"):
  704. continue
  705. if not m.file_url:
  706. continue
  707. detail_media.append({
  708. "id": m.id,
  709. "contentType": m.content_type,
  710. "fileUrl": m.file_url,
  711. "thumbnailUrl": m.thumbnail_url,
  712. "description": m.description,
  713. "sortOrder": m.sort_order,
  714. })
  715. product_payload = {
  716. "id": product.id,
  717. "name": product.name,
  718. "skuCode": product.sku_code,
  719. "categoryId": product.category_id,
  720. "categoryName": cat_name,
  721. "unitId": line.unit_id,
  722. "unitName": unit.name if unit else None,
  723. "measureValue": float(product.measure_value) if getattr(product, "measure_value", None) is not None else None,
  724. "spec": getattr(product, "spec", None) or None,
  725. "price": float(line.price) if line.price else 0,
  726. "imageUrl": product.image_url,
  727. "description": product.description or "",
  728. "detailContent": product.detail_content or "",
  729. "isActive": bool(getattr(product, "is_active", 1)),
  730. }
  731. return success_response({
  732. "product": product_payload,
  733. "banners": banners,
  734. "parameterValues": parameter_values,
  735. "detailMedia": detail_media,
  736. })
  737. @router.get("/products-ro/{product_id}", response_model=dict)
  738. def get_client_product_detail_readonly(
  739. product_id: str,
  740. db: Session = Depends(get_db),
  741. current_user: dict = Depends(require_client),
  742. ):
  743. """客户端只读商品详情:用于订单历史等场景,不依赖区域价目表。"""
  744. product = product_crud.get(db, product_id)
  745. if not product or getattr(product, "is_del", 0) == 1:
  746. return error_response(404, "商品不存在")
  747. unit = unit_crud.get(db, product.unit_id) if getattr(product, "unit_id", None) else None
  748. cat_name = ""
  749. if getattr(product, "category_id", None):
  750. cat = category_crud.get(db, product.category_id)
  751. cat_name = cat.name if cat else ""
  752. banners_raw = product_banner.get_by_product(db, product_id)
  753. banners = [{
  754. "mediaType": b.media_type,
  755. "fileUrl": b.file_url,
  756. "thumbnailUrl": b.thumbnail_url,
  757. "sortOrder": b.sort_order,
  758. } for b in banners_raw]
  759. pv_rows = product_parameter_value.get_by_product_with_detail(db, product_id)
  760. parameter_values = []
  761. for row in pv_rows:
  762. val = row.get("param_value")
  763. if val is None or str(val).strip() == "":
  764. continue
  765. parameter_values.append({
  766. "parameterId": row.get("parameter_id"),
  767. "parameterName": row.get("parameter_name") or "",
  768. "paramValue": str(val),
  769. })
  770. media_raw = product_detail_media.get_by_product(db, product_id)
  771. detail_media = []
  772. for m in media_raw:
  773. if m.content_type not in ("image", "video"):
  774. continue
  775. if not m.file_url:
  776. continue
  777. detail_media.append({
  778. "id": m.id,
  779. "contentType": m.content_type,
  780. "fileUrl": m.file_url,
  781. "thumbnailUrl": m.thumbnail_url,
  782. "description": m.description,
  783. "sortOrder": m.sort_order,
  784. })
  785. product_payload = {
  786. "id": product.id,
  787. "name": product.name,
  788. "skuCode": product.sku_code,
  789. "categoryId": product.category_id,
  790. "categoryName": cat_name,
  791. "unitId": getattr(product, "unit_id", None),
  792. "unitName": unit.name if unit else None,
  793. "measureValue": float(product.measure_value) if getattr(product, "measure_value", None) is not None else None,
  794. "spec": getattr(product, "spec", None) or None,
  795. # 价格由前端在只读订单场景使用历史成交价(fallbackPrice)展示,这里不做价目表绑定
  796. "price": 0,
  797. "imageUrl": product.image_url,
  798. "description": product.description or "",
  799. "detailContent": product.detail_content or "",
  800. "isActive": bool(getattr(product, "is_active", 1)),
  801. }
  802. return success_response({
  803. "product": product_payload,
  804. "banners": banners,
  805. "parameterValues": parameter_values,
  806. "detailMedia": detail_media,
  807. })
  808. @router.post("/orders/from-cart", response_model=dict)
  809. def create_order_from_cart(
  810. request: dict,
  811. db: Session = Depends(get_db),
  812. current_user: dict = Depends(require_client),
  813. ):
  814. payload = _resolve_request_payload(request, current_user)
  815. address_id = payload.get("addressId")
  816. payment_method = order_crud.normalize_payment_method(payload.get("paymentMethod", "online"))
  817. remark = payload.get("remark")
  818. if not address_id:
  819. return error_response(400, "请选择配送地址")
  820. uid = current_user.get("user_id")
  821. user = user_crud.get(db, uid)
  822. if not user or not user.region_id:
  823. return error_response(400, "用户未绑定区域")
  824. creator = {"user_id": uid, "real_name": current_user.get("real_name")}
  825. client_cart_crud.sync_to_active_price_list(db, uid, user.region_id, creator)
  826. pl = price_list_crud.get_active(db, user.region_id)
  827. if not pl:
  828. return error_response(404, "当前区域无生效价格清单")
  829. lines = client_cart_crud.list_by_client(db, uid)
  830. if not lines:
  831. return error_response(400, "购物车为空")
  832. price_items = price_list_item_crud.get_by_price_list(db, pl.id)
  833. pmap = {i.product_id: i for i in price_items}
  834. address = address_crud.get(db, address_id)
  835. if not address or address.user_id != uid:
  836. return error_response(404, "地址不存在")
  837. order_items = []
  838. for ln in lines:
  839. if ln.product_id not in pmap:
  840. return error_response(
  841. 400,
  842. "购物车中含有已不在当前区域价格清单的商品,请返回购物车刷新后移除无效商品再下单",
  843. )
  844. if ln.price_list_id != pl.id:
  845. return error_response(400, "购物车价格已更新,请返回购物车刷新后再下单")
  846. order_items.append({
  847. "productId": ln.product_id,
  848. "unitId": ln.unit_id,
  849. "quantity": float(ln.quantity),
  850. "unitPrice": float(ln.unit_price),
  851. })
  852. price_list_id = pl.id
  853. full_address = f"{address.province or ''}{address.city or ''}{address.district or ''}{address.detail_address or ''}"
  854. order = order_crud.create_order(
  855. db,
  856. client_id=current_user.get("user_id"),
  857. region_id=user.region_id,
  858. price_list_id=price_list_id,
  859. price_version=pl.version,
  860. items=order_items,
  861. address_id=address_id,
  862. address_info={
  863. "contactPerson": address.contact_person,
  864. "contactPhone": address.contact_phone,
  865. "fullAddress": full_address,
  866. },
  867. payment_method=payment_method,
  868. remark=remark,
  869. creator=creator,
  870. )
  871. client_cart_crud.clear(db, current_user.get("user_id"), creator)
  872. return success_response({
  873. "orderId": order.id,
  874. "orderNo": order.order_no,
  875. "totalAmount": float(order.total_amount),
  876. "status": order.status,
  877. "paymentMethod": order.payment_method,
  878. })
  879. @router.get("/dashboard/statistics", response_model=dict)
  880. def client_dashboard_stats(
  881. db: Session = Depends(get_db),
  882. current_user: dict = Depends(require_client),
  883. ):
  884. cid = current_user.get("user_id")
  885. today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
  886. orders, _ = order_crud.get_multi_with_filters(db, client_id=cid, skip=0, limit=5000)
  887. pending_pay = sum(1 for o in orders if o.status == "pending_payment")
  888. delivering = sum(1 for o in orders if getattr(o, "order_status", None) == "pending_confirm")
  889. completed = sum(1 for o in orders if o.status == "completed")
  890. total_amt = sum(float(o.total_amount or 0) for o in orders if o.create_at and o.create_at >= today)
  891. return success_response({
  892. "pendingPayment": pending_pay,
  893. "delivering": delivering,
  894. "completed": completed,
  895. "todayOrderAmount": total_amt,
  896. })
  897. @router.get("/invoices", response_model=dict)
  898. def list_client_invoices(
  899. db: Session = Depends(get_db),
  900. current_user: dict = Depends(require_client),
  901. page: int = Query(1, ge=1),
  902. pageSize: int = Query(10, ge=1, le=100),
  903. status: Optional[str] = None,
  904. ):
  905. q = db.query(BizInvoice).filter(
  906. and_(BizInvoice.client_id == current_user.get("user_id"), BizInvoice.is_del == 0)
  907. )
  908. if status:
  909. q = q.filter(BizInvoice.status == status)
  910. total = q.count()
  911. rows = q.order_by(BizInvoice.create_at.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
  912. lst = [{
  913. "id": r.id,
  914. "invoiceNo": r.invoice_no,
  915. "orderId": r.order_id,
  916. "title": r.title,
  917. "invoiceAmount": float(r.invoice_amount) if r.invoice_amount else 0,
  918. "status": r.status,
  919. "createAt": str(r.create_at) if r.create_at else None,
  920. } for r in rows]
  921. return success_response({
  922. "list": lst,
  923. "total": total,
  924. "page": page,
  925. "pageSize": pageSize,
  926. "totalPages": (total + pageSize - 1) // pageSize if total else 0,
  927. })
  928. @router.get("/invoices/summary", response_model=dict)
  929. def invoice_summary(
  930. db: Session = Depends(get_db),
  931. current_user: dict = Depends(require_client),
  932. ):
  933. rows = db.query(BizInvoice).filter(
  934. and_(BizInvoice.client_id == current_user.get("user_id"), BizInvoice.is_del == 0)
  935. ).all()
  936. issued = [r for r in rows if r.status == "issued"]
  937. total_issued = sum(float(r.invoice_amount or 0) for r in issued)
  938. return success_response({
  939. "totalInvoicedAmount": total_issued,
  940. "invoiceCount": len(rows),
  941. })
  942. @router.get("/invoices/available-orders", response_model=dict)
  943. def invoices_available_orders(
  944. db: Session = Depends(get_db),
  945. current_user: dict = Depends(require_client),
  946. ):
  947. """可申请开票的已完成订单(简化:已完成且尚无发票)"""
  948. orders, _ = order_crud.get_multi_with_filters(
  949. db, client_id=current_user.get("user_id"), status="completed", skip=0, limit=200
  950. )
  951. out = []
  952. for o in orders:
  953. inv = db.query(BizInvoice).filter(
  954. and_(BizInvoice.order_id == o.id, BizInvoice.is_del == 0)
  955. ).first()
  956. if inv:
  957. continue
  958. out.append({"orderId": o.id, "orderNo": o.order_no, "totalAmount": float(o.total_amount or 0)})
  959. return success_response({"list": out})
  960. @router.get("/invoices/{invoice_id}", response_model=dict)
  961. def get_client_invoice(
  962. invoice_id: str,
  963. db: Session = Depends(get_db),
  964. current_user: dict = Depends(require_client),
  965. ):
  966. r = db.query(BizInvoice).filter(
  967. and_(BizInvoice.id == invoice_id, BizInvoice.client_id == current_user.get("user_id"), BizInvoice.is_del == 0)
  968. ).first()
  969. if not r:
  970. return error_response(404, "发票不存在")
  971. return success_response({
  972. "id": r.id,
  973. "invoiceNo": r.invoice_no,
  974. "orderId": r.order_id,
  975. "title": r.title,
  976. "invoiceAmount": float(r.invoice_amount) if r.invoice_amount else 0,
  977. "status": r.status,
  978. "invoiceUrl": r.invoice_url,
  979. "createAt": str(r.create_at) if r.create_at else None,
  980. })
  981. @router.post("/invoices", response_model=dict)
  982. def apply_invoice(
  983. request: dict,
  984. db: Session = Depends(get_db),
  985. current_user: dict = Depends(require_client),
  986. ):
  987. """申请开票(简化)"""
  988. payload = _resolve_request_payload(request, current_user)
  989. order_id = payload.get("orderId")
  990. title = payload.get("title") or ""
  991. invoice_type = payload.get("invoiceType") or "normal"
  992. if not order_id:
  993. return error_response(400, "缺少 orderId")
  994. order = order_crud.get(db, order_id)
  995. if not order or order.client_id != current_user.get("user_id"):
  996. return error_response(404, "订单不存在")
  997. if order.status != "completed":
  998. return error_response(400, "仅已完成订单可申请开票")
  999. inv = BizInvoice(
  1000. id=generate_id(),
  1001. invoice_no=f"INV{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}",
  1002. order_id=order.id,
  1003. order_no=order.order_no,
  1004. client_id=order.client_id,
  1005. client_name=current_user.get("real_name") or "",
  1006. invoice_type=invoice_type,
  1007. title=title or "个人",
  1008. invoice_amount=order.total_amount,
  1009. tax_rate=Decimal("0"),
  1010. tax_amount=Decimal("0"),
  1011. status="pending",
  1012. create_by=current_user.get("user_id"),
  1013. create_name=current_user.get("real_name"),
  1014. )
  1015. db.add(inv)
  1016. db.commit()
  1017. db.refresh(inv)
  1018. return success_response({"id": inv.id, "invoiceNo": inv.invoice_no, "status": inv.status})
  1019. @router.get("/payment-config", response_model=dict)
  1020. def client_payment_config():
  1021. """小程序支付配置(不含密钥)"""
  1022. return success_response({
  1023. "appId": settings.WXPAY_MINI_APPID,
  1024. "enabled": bool(settings.WXPAY_MCHID and settings.WXPAY_MINI_APPID),
  1025. })
  1026. @router.post("/orders/{order_id}/wechat-pay", response_model=dict)
  1027. async def mini_order_wechat_pay(
  1028. order_id: str,
  1029. request: dict,
  1030. db: Session = Depends(get_db),
  1031. current_user: dict = Depends(require_client),
  1032. ):
  1033. """调起微信支付参数(需 openid)"""
  1034. from app.core.wechat_pay import get_wechat_pay_v2
  1035. payload = _resolve_request_payload(request, current_user)
  1036. openid = payload.get("openid")
  1037. if not openid:
  1038. return error_response(400, "缺少 openid")
  1039. order = order_crud.get(db, order_id)
  1040. if not order or order.client_id != current_user.get("user_id"):
  1041. return error_response(404, "订单不存在")
  1042. if getattr(order, "payment_method", None) != "online":
  1043. return error_response(400, "线下支付订单无需微信支付")
  1044. if order.status != "pending_payment":
  1045. return error_response(400, "订单状态不允许支付")
  1046. total_amount = int(float(order.total_amount) * 100)
  1047. if total_amount <= 0:
  1048. return error_response(400, "订单金额必须大于0")
  1049. pay_client = get_wechat_pay_v2()
  1050. result = pay_client.unified_order(
  1051. out_trade_no=order.order_no,
  1052. total_fee=total_amount,
  1053. body="陇耘鲜达-订单支付",
  1054. openid=openid,
  1055. trade_type="JSAPI",
  1056. )
  1057. if result.get("success"):
  1058. pp = result.get("pay_params") or {}
  1059. _upsert_wechat_pay_pending(
  1060. db,
  1061. order_id=str(order.id),
  1062. out_trade_no=order.order_no,
  1063. total_fee=total_amount,
  1064. payer_openid=openid,
  1065. operator_id=current_user.get("user_id"),
  1066. operator_name=current_user.get("real_name"),
  1067. )
  1068. db.commit()
  1069. return success_response({
  1070. "orderId": order.id,
  1071. "orderNo": order.order_no,
  1072. "prepayId": result.get("prepay_id"),
  1073. "appId": pp.get("appId"),
  1074. "timeStamp": pp.get("timeStamp"),
  1075. "nonceStr": pp.get("nonceStr"),
  1076. "package": pp.get("package"),
  1077. "signType": pp.get("signType"),
  1078. "paySign": pp.get("paySign"),
  1079. })
  1080. return error_response(500, result.get("error", "创建支付失败"))