mini_supplier.py 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354
  1. """小程序端供应商接口"""
  2. from decimal import Decimal
  3. from typing import Optional
  4. from datetime import datetime
  5. from fastapi import APIRouter, Depends, Request, Query
  6. from sqlalchemy.orm import Session
  7. from app.core.database import get_db
  8. from app.core.auth import get_current_user, get_client_ip
  9. from app.core.response import success_response, error_response
  10. from app.models.models import BasAddress
  11. from app.crud.address import address_crud
  12. from app.crud.user import user_crud
  13. from app.crud.region import region_crud
  14. from app.crud.attachment import attachment_crud
  15. from app.models.models import BizAttachment
  16. router = APIRouter(prefix="/api/mini/supplier", tags=["小程序端-供应商接口"])
  17. def _supplier_quote_unit_yuan_from_pli(pli) -> Optional[float]:
  18. """价格清单行 price 为面向客户的最终单价;pricing_coefficient 为调价系数(%),供货单价 = price / (1 + 系数/100)。"""
  19. if pli is None or getattr(pli, "price", None) is None:
  20. return None
  21. final_price = Decimal(str(pli.price))
  22. raw_coeff = getattr(pli, "pricing_coefficient", None)
  23. if raw_coeff is None:
  24. coeff_pct = Decimal("0")
  25. else:
  26. try:
  27. coeff_pct = Decimal(str(raw_coeff))
  28. except Exception:
  29. coeff_pct = Decimal("0")
  30. factor = Decimal("1") + coeff_pct / Decimal("100")
  31. if factor <= Decimal("0"):
  32. factor = Decimal("1")
  33. supplier_unit = (final_price / factor).quantize(Decimal("0.01"))
  34. return float(supplier_unit)
  35. def require_supplier(current_user: dict = Depends(get_current_user)):
  36. """验证供应商权限"""
  37. if current_user.get("role") != "supplier":
  38. return error_response(403, "需要供应商权限")
  39. return current_user
  40. def _normalize_image_urls(raw) -> list[str]:
  41. out: list[str] = []
  42. if isinstance(raw, list):
  43. for x in raw:
  44. s = str(x or "").strip()
  45. if s:
  46. out.append(s)
  47. elif isinstance(raw, str):
  48. s = raw.strip()
  49. if s:
  50. out.append(s)
  51. return out
  52. def _replace_delivery_prepare_attachments(
  53. db: Session,
  54. *,
  55. task_id: str,
  56. image_urls: list[str],
  57. uploader_id: Optional[str],
  58. uploader_name: Optional[str],
  59. ):
  60. rows = db.query(BizAttachment).filter(
  61. BizAttachment.business_type == "delivery_prepare",
  62. BizAttachment.business_id == task_id,
  63. BizAttachment.is_del == 0,
  64. ).all()
  65. now = datetime.now()
  66. for r in rows:
  67. r.is_del = 1
  68. r.update_at = now
  69. r.update_by = uploader_id
  70. db.flush()
  71. for idx, url in enumerate(image_urls, start=1):
  72. attachment_crud.create_attachment(
  73. db=db,
  74. business_type="delivery_prepare",
  75. business_id=task_id,
  76. file_name=f"delivery_prepare_{idx}.jpg",
  77. file_url=url,
  78. file_type="image/jpeg",
  79. upload_by=uploader_id,
  80. upload_by_name=uploader_name,
  81. )
  82. # ============ 地址管理 ============
  83. @router.get("/addresses", response_model=dict)
  84. def list_addresses(
  85. db: Session = Depends(get_db),
  86. current_user: dict = Depends(require_supplier)
  87. ):
  88. """获取供应商取货地址列表"""
  89. addresses = address_crud.get_by_user(db, current_user.get("user_id"))
  90. return success_response([{
  91. "id": addr.id,
  92. "userId": addr.user_id,
  93. "addressType": addr.address_type,
  94. "contactPerson": addr.contact_person,
  95. "contactPhone": addr.contact_phone,
  96. "province": addr.province,
  97. "city": addr.city,
  98. "district": addr.district,
  99. "detailAddress": addr.detail_address,
  100. "isDefault": addr.is_default,
  101. "createAt": str(addr.create_at) if addr.create_at else None
  102. } for addr in addresses])
  103. @router.get("/pickup-addresses", response_model=dict)
  104. def list_pickup_addresses(
  105. db: Session = Depends(get_db),
  106. current_user: dict = Depends(require_supplier)
  107. ):
  108. """获取供应商取货地址列表"""
  109. addresses = address_crud.get_by_user_and_type(db, current_user.get("user_id"), "pickup")
  110. return success_response([{
  111. "id": addr.id,
  112. "userId": addr.user_id,
  113. "addressType": addr.address_type,
  114. "contactPerson": addr.contact_person,
  115. "contactPhone": addr.contact_phone,
  116. "province": addr.province,
  117. "city": addr.city,
  118. "district": addr.district,
  119. "detailAddress": addr.detail_address,
  120. "isDefault": addr.is_default
  121. } for addr in addresses])
  122. @router.get("/addresses/{address_id}", response_model=dict)
  123. def get_address(
  124. address_id: str,
  125. db: Session = Depends(get_db),
  126. current_user: dict = Depends(require_supplier)
  127. ):
  128. """获取地址详情"""
  129. address = address_crud.get(db, address_id)
  130. if not address or address.user_id != current_user.get("user_id"):
  131. return error_response(404, "地址不存在")
  132. return success_response({
  133. "id": address.id,
  134. "userId": address.user_id,
  135. "addressType": address.address_type,
  136. "contactPerson": address.contact_person,
  137. "contactPhone": address.contact_phone,
  138. "province": address.province,
  139. "city": address.city,
  140. "district": address.district,
  141. "detailAddress": address.detail_address,
  142. "isDefault": address.is_default,
  143. "createAt": str(address.create_at) if address.create_at else None
  144. })
  145. @router.post("/addresses", response_model=dict)
  146. def create_address(
  147. request: dict,
  148. db: Session = Depends(get_db),
  149. current_user: dict = Depends(require_supplier)
  150. ):
  151. """创建地址"""
  152. request["user_id"] = current_user.get("user_id")
  153. creator = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  154. # 如果设置为默认,先取消其他默认
  155. if request.get("isDefault"):
  156. address_crud.set_default(db, current_user.get("user_id"), "")
  157. address = address_crud.create(db, request, creator)
  158. return success_response({
  159. "id": address.id,
  160. "addressType": address.address_type,
  161. "contactPerson": address.contact_person,
  162. "contactPhone": address.contact_phone
  163. })
  164. @router.put("/addresses/{address_id}", response_model=dict)
  165. def update_address(
  166. address_id: str,
  167. request: dict,
  168. db: Session = Depends(get_db),
  169. current_user: dict = Depends(require_supplier)
  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. # 如果设置为默认,先取消其他默认
  176. if request.get("isDefault"):
  177. address_crud.set_default(db, current_user.get("user_id"), address_id)
  178. updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  179. address = address_crud.update_with_history(db, address_id, request, updater)
  180. return success_response({
  181. "id": address.id,
  182. "addressType": address.address_type,
  183. "contactPerson": address.contact_person,
  184. "contactPhone": address.contact_phone
  185. })
  186. @router.delete("/addresses/{address_id}", response_model=dict)
  187. def delete_address(
  188. address_id: str,
  189. db: Session = Depends(get_db),
  190. current_user: dict = Depends(require_supplier)
  191. ):
  192. """删除地址"""
  193. address = address_crud.get(db, address_id)
  194. if not address or address.user_id != current_user.get("user_id"):
  195. return error_response(404, "地址不存在")
  196. success = address_crud.soft_delete_with_history(db, address_id)
  197. return success_response({"id": address_id})
  198. @router.post("/addresses/{address_id}/set-default", response_model=dict)
  199. def set_default_address(
  200. address_id: str,
  201. db: Session = Depends(get_db),
  202. current_user: dict = Depends(require_supplier)
  203. ):
  204. """设置默认地址"""
  205. address = address_crud.get(db, address_id)
  206. if not address or address.user_id != current_user.get("user_id"):
  207. return error_response(404, "地址不存在")
  208. success = address_crud.set_default(db, current_user.get("user_id"), address_id)
  209. if success:
  210. return success_response({"message": "设置成功"})
  211. return error_response(400, "设置失败")
  212. # ============ 个人资料 ============
  213. @router.get("/profile", response_model=dict)
  214. def get_profile(
  215. db: Session = Depends(get_db),
  216. current_user: dict = Depends(require_supplier)
  217. ):
  218. """获取供应商个人资料"""
  219. user = user_crud.get(db, current_user.get("user_id"))
  220. if not user:
  221. return error_response(404, "用户不存在")
  222. region_name = None
  223. if user.region_id:
  224. region = region_crud.get(db, user.region_id)
  225. region_name = region.name if region else None
  226. # 获取默认取货地址
  227. default_address = address_crud.get_default(db, user.id, "pickup")
  228. address_info = None
  229. if default_address:
  230. address_info = {
  231. "contactPerson": default_address.contact_person,
  232. "contactPhone": default_address.contact_phone,
  233. "province": default_address.province,
  234. "city": default_address.city,
  235. "district": default_address.district,
  236. "detailAddress": default_address.detail_address
  237. }
  238. return success_response({
  239. "id": user.id,
  240. "username": user.username,
  241. "realName": user.real_name,
  242. "phone": user.phone,
  243. "role": user.role,
  244. "regionId": user.region_id,
  245. "regionName": region_name,
  246. "address": address_info
  247. })
  248. @router.put("/profile", response_model=dict)
  249. def update_profile(
  250. request: dict,
  251. db: Session = Depends(get_db),
  252. current_user: dict = Depends(require_supplier)
  253. ):
  254. """更新供应商个人资料"""
  255. user = user_crud.get(db, current_user.get("user_id"))
  256. if not user:
  257. return error_response(404, "用户不存在")
  258. updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
  259. user = user_crud.update_with_history(db, current_user.get("user_id"), request, updater)
  260. return success_response({
  261. "id": user.id,
  262. "realName": user.real_name,
  263. "phone": user.phone
  264. })
  265. # ============ 询价单管理 ============
  266. @router.get("/inquiries", response_model=dict)
  267. def list_inquiries(
  268. page: int = Query(1, ge=1, description="页码"),
  269. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  270. db: Session = Depends(get_db),
  271. current_user: dict = Depends(require_supplier)
  272. ):
  273. """获取供应商可响应的询价单列表"""
  274. from app.crud.inquiry import inquiry_crud, inquiry_item_crud
  275. from app.crud.quotation import quotation_crud
  276. user = user_crud.get(db, current_user.get("user_id"))
  277. if not user or not user.region_id:
  278. return error_response(400, "用户未绑定区域")
  279. inquiries, total = inquiry_crud.get_multi_with_filters(
  280. db,
  281. region_id=user.region_id,
  282. status="published",
  283. skip=(page - 1) * pageSize,
  284. limit=pageSize
  285. )
  286. inquiry_list = []
  287. for inquiry in inquiries:
  288. has_quoted = quotation_crud.get_latest_by_inquiry_and_supplier(db, inquiry.id, user.id) is not None
  289. inquiry_list.append({
  290. "id": inquiry.id,
  291. "title": inquiry.title,
  292. "regionId": inquiry.region_id,
  293. "status": inquiry.status,
  294. "publishTime": str(inquiry.publish_time) if inquiry.publish_time else None,
  295. "closeTime": str(inquiry.close_time) if inquiry.close_time else None,
  296. "remark": inquiry.remark,
  297. "createAt": str(inquiry.create_at) if inquiry.create_at else None,
  298. "hasQuoted": has_quoted
  299. })
  300. return success_response({
  301. "list": inquiry_list,
  302. "total": total,
  303. "page": page,
  304. "pageSize": pageSize,
  305. "totalPages": (total + pageSize - 1) // pageSize
  306. })
  307. @router.get("/inquiries/{inquiry_id}", response_model=dict)
  308. def get_inquiry_detail(
  309. inquiry_id: str,
  310. db: Session = Depends(get_db),
  311. current_user: dict = Depends(require_supplier)
  312. ):
  313. """获取询价单详情"""
  314. from app.crud.inquiry import inquiry_crud, inquiry_item_crud
  315. from app.crud.product import product_crud
  316. from app.crud.category import category_crud
  317. from app.crud.unit import unit_crud
  318. from app.crud.quotation import quotation_crud
  319. user = user_crud.get(db, current_user.get("user_id"))
  320. if not user or not user.region_id:
  321. return error_response(400, "用户未绑定区域")
  322. inquiry = inquiry_crud.get(db, inquiry_id)
  323. if not inquiry:
  324. return error_response(404, "询价单不存在")
  325. if inquiry.region_id != user.region_id:
  326. return error_response(403, "无权查看此询价单")
  327. items = inquiry_item_crud.get_by_inquiry(db, inquiry_id)
  328. quotation = quotation_crud.get_latest_by_inquiry_and_supplier(db, inquiry_id, user.id)
  329. latest_quote_price_map = {}
  330. if quotation:
  331. from app.crud.quotation import quotation_item_crud
  332. quotation_items = quotation_item_crud.get_by_quotation(db, quotation.id)
  333. for quote_item in quotation_items:
  334. latest_quote_price_map[quote_item.product_id] = float(quote_item.price) if quote_item.price else 0
  335. item_details = []
  336. for item in items:
  337. product = product_crud.get(db, item.product_id)
  338. category_name = None
  339. first_category_id = None
  340. first_category_name = None
  341. first_category_image = None
  342. first_category_sort_order = 0
  343. second_category_id = None
  344. second_category_name = None
  345. second_category_image = None
  346. second_category_sort_order = 0
  347. product_sort_order = int(getattr(product, "sort_order", None) or 0) if product else 0
  348. unit_name = None
  349. if product and product.category_id:
  350. category = category_crud.get(db, product.category_id)
  351. category_name = category.name if category else None
  352. if category:
  353. if category.parent_id:
  354. parent_category = category_crud.get(db, category.parent_id)
  355. first_category_id = parent_category.id if parent_category else category.id
  356. first_category_name = parent_category.name if parent_category else category.name
  357. first_category_image = parent_category.image_url if parent_category else category.image_url
  358. first_category_sort_order = int(parent_category.sort_order or 0) if parent_category else int(category.sort_order or 0)
  359. second_category_id = category.id
  360. second_category_name = category.name
  361. second_category_image = category.image_url
  362. second_category_sort_order = int(category.sort_order or 0)
  363. else:
  364. first_category_id = category.id
  365. first_category_name = category.name
  366. first_category_image = category.image_url
  367. first_category_sort_order = int(category.sort_order or 0)
  368. second_category_sort_order = int(category.sort_order or 0)
  369. if product and product.unit_id:
  370. unit = unit_crud.get(db, product.unit_id)
  371. unit_name = unit.name if unit else None
  372. item_details.append({
  373. "id": item.id,
  374. "productId": item.product_id,
  375. "productName": product.name if product else None,
  376. "description": product.description if product else None,
  377. "imageUrl": product.image_url if product else None,
  378. "categoryId": product.category_id if product else None,
  379. "categoryName": category_name,
  380. "firstCategoryId": first_category_id,
  381. "firstCategoryName": first_category_name,
  382. "firstCategoryImage": first_category_image,
  383. "secondCategoryId": second_category_id,
  384. "secondCategoryName": second_category_name,
  385. "secondCategoryImage": second_category_image,
  386. "unitId": product.unit_id if product else None,
  387. "unitName": unit_name,
  388. "measureValue": float(product.measure_value) if product and product.measure_value is not None else None,
  389. "lastPrice": latest_quote_price_map.get(item.product_id),
  390. "createAt": str(item.create_at) if item.create_at else None,
  391. "_firstCategorySortOrder": first_category_sort_order,
  392. "_secondCategorySortOrder": second_category_sort_order,
  393. "_productSortOrder": product_sort_order
  394. })
  395. # 与客户端首页商品区一致:一级分类排序 -> 二级分类排序 -> 商品排序 -> 名称 -> id
  396. item_details.sort(
  397. key=lambda row: (
  398. int(row.get("_firstCategorySortOrder") or 0),
  399. int(row.get("_secondCategorySortOrder") or 0),
  400. int(row.get("_productSortOrder") or 0),
  401. (row.get("productName") or "").lower(),
  402. row.get("productId") or "",
  403. )
  404. )
  405. for row in item_details:
  406. row.pop("_firstCategorySortOrder", None)
  407. row.pop("_secondCategorySortOrder", None)
  408. row.pop("_productSortOrder", None)
  409. return success_response({
  410. "id": inquiry.id,
  411. "title": inquiry.title,
  412. "regionId": inquiry.region_id,
  413. "status": inquiry.status,
  414. "publishTime": str(inquiry.publish_time) if inquiry.publish_time else None,
  415. "closeTime": str(inquiry.close_time) if inquiry.close_time else None,
  416. "remark": inquiry.remark,
  417. "createAt": str(inquiry.create_at) if inquiry.create_at else None,
  418. "items": item_details,
  419. "hasQuoted": quotation is not None,
  420. "quotationId": quotation.id if quotation else None,
  421. "quotationStatus": quotation.status if quotation else None
  422. })
  423. # ============ 报价单管理 ============
  424. @router.post("/quotations", response_model=dict)
  425. def submit_quotation(
  426. request: dict,
  427. db: Session = Depends(get_db),
  428. current_user: dict = Depends(require_supplier)
  429. ):
  430. """提交报价单"""
  431. from app.crud.inquiry import inquiry_crud, inquiry_item_crud
  432. from app.crud.quotation import quotation_crud, quotation_item_crud
  433. from app.crud.product import product_crud
  434. from decimal import Decimal
  435. user = user_crud.get(db, current_user.get("user_id"))
  436. if not user or not user.region_id:
  437. return error_response(400, "用户未绑定区域")
  438. inquiry_id = request.get("inquiryId")
  439. items = request.get("items", [])
  440. if not inquiry_id:
  441. return error_response(400, "询价单ID不能为空")
  442. if not items:
  443. return error_response(400, "报价项不能为空")
  444. inquiry = inquiry_crud.get(db, inquiry_id)
  445. if not inquiry:
  446. return error_response(404, "询价单不存在")
  447. if inquiry.region_id != user.region_id:
  448. return error_response(403, "无权对此询价单报价")
  449. if inquiry.status != "published":
  450. return error_response(400, "询价单已关闭")
  451. # 支持同一询价单多次报价:每次提交都创建一张新报价单
  452. inquiry_items = inquiry_item_crud.get_by_inquiry(db, inquiry_id)
  453. inquiry_product_ids = set(item.product_id for item in inquiry_items)
  454. submitted_product_ids = set(item.get("productId") for item in items)
  455. if submitted_product_ids != inquiry_product_ids:
  456. return error_response(400, "报价商品必须与询价商品一致")
  457. creator = {"user_id": user.id, "real_name": user.real_name}
  458. quotation_data = {
  459. "inquiry_id": inquiry_id,
  460. "supplier_id": user.id,
  461. "region_id": user.region_id,
  462. "status": "submitted",
  463. "submit_time": datetime.now()
  464. }
  465. quotation_items_data = []
  466. total_amount = Decimal("0")
  467. for item in items:
  468. product_id = item.get("productId")
  469. unit_id = item.get("unitId")
  470. if not unit_id:
  471. product = product_crud.get(db, product_id) if product_id else None
  472. unit_id = product.unit_id if product else None
  473. if not unit_id:
  474. return error_response(400, f"商品 {product_id} 缺少单位信息,请传 unitId")
  475. price = Decimal(str(item.get("price", 0)))
  476. total_amount += price
  477. quotation_items_data.append({
  478. "product_id": product_id,
  479. "unit_id": unit_id,
  480. "price": price
  481. })
  482. quotation_data["total_amount"] = total_amount
  483. quotation = quotation_crud.create(db, quotation_data, creator)
  484. quotation_item_crud.create_items(db, quotation.id, quotation_items_data, creator)
  485. return success_response({
  486. "id": quotation.id,
  487. "totalAmount": float(quotation.total_amount),
  488. "status": quotation.status,
  489. "submitTime": str(quotation.submit_time) if quotation.submit_time else None
  490. })
  491. @router.get("/quotations", response_model=dict)
  492. def list_my_quotations(
  493. page: int = Query(1, ge=1, description="页码"),
  494. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  495. inquiryId: Optional[str] = Query(None, description="询价单ID"),
  496. status: Optional[str] = Query(None, description="报价状态,如 selected=已采纳"),
  497. db: Session = Depends(get_db),
  498. current_user: dict = Depends(require_supplier)
  499. ):
  500. """获取我的报价列表"""
  501. from app.crud.inquiry import inquiry_crud
  502. from app.crud.quotation import quotation_crud
  503. user = user_crud.get(db, current_user.get("user_id"))
  504. if not user:
  505. return error_response(404, "用户不存在")
  506. quotations, total = quotation_crud.get_multi_with_filters(
  507. db,
  508. supplier_id=user.id,
  509. inquiry_id=inquiryId,
  510. status=status,
  511. skip=(page - 1) * pageSize,
  512. limit=pageSize
  513. )
  514. inquiry_map = {}
  515. quotation_list = []
  516. for q in quotations:
  517. if q.inquiry_id and q.inquiry_id not in inquiry_map:
  518. inquiry = inquiry_crud.get(db, q.inquiry_id)
  519. inquiry_map[q.inquiry_id] = inquiry.title if inquiry else None
  520. quotation_list.append({
  521. "id": q.id,
  522. "inquiryId": q.inquiry_id,
  523. "inquiryTitle": inquiry_map.get(q.inquiry_id),
  524. "status": q.status,
  525. "totalAmount": float(q.total_amount) if q.total_amount else 0,
  526. "submitTime": str(q.submit_time) if q.submit_time else None,
  527. })
  528. return success_response({
  529. "list": quotation_list,
  530. "total": total,
  531. "page": page,
  532. "pageSize": pageSize,
  533. "totalPages": (total + pageSize - 1) // pageSize
  534. })
  535. @router.get("/quotations/{quotation_id}", response_model=dict)
  536. def get_my_quotation(
  537. quotation_id: str,
  538. db: Session = Depends(get_db),
  539. current_user: dict = Depends(require_supplier)
  540. ):
  541. """获取我的报价单详情"""
  542. from app.crud.inquiry import inquiry_crud, inquiry_item_crud
  543. from app.crud.quotation import quotation_crud, quotation_item_crud
  544. from app.crud.product import product_crud
  545. user = user_crud.get(db, current_user.get("user_id"))
  546. if not user:
  547. return error_response(404, "用户不存在")
  548. quotation = quotation_crud.get(db, quotation_id)
  549. if not quotation:
  550. return error_response(404, "报价单不存在")
  551. if quotation.supplier_id != user.id:
  552. return error_response(403, "无权查看此报价单")
  553. items = quotation_item_crud.get_by_quotation(db, quotation_id)
  554. inquiry_title = None
  555. if quotation.inquiry_id:
  556. inquiry = inquiry_crud.get(db, quotation.inquiry_id)
  557. inquiry_title = inquiry.title if inquiry else None
  558. item_details = []
  559. for item in items:
  560. product = product_crud.get(db, item.product_id)
  561. item_details.append({
  562. "id": item.id,
  563. "productId": item.product_id,
  564. "productName": product.name if product else None,
  565. "unitId": item.unit_id,
  566. "price": float(item.price) if item.price else 0,
  567. "createAt": str(item.create_at) if item.create_at else None
  568. })
  569. return success_response({
  570. "id": quotation.id,
  571. "inquiryId": quotation.inquiry_id,
  572. "inquiryTitle": inquiry_title,
  573. "status": quotation.status,
  574. "totalAmount": float(quotation.total_amount) if quotation.total_amount else 0,
  575. "submitTime": str(quotation.submit_time) if quotation.submit_time else None,
  576. "items": item_details
  577. })
  578. # ============ 价格清单 - 供应商查看 ============
  579. @router.get("/price-lists", response_model=dict)
  580. def list_my_price_lists(
  581. page: int = Query(1, ge=1, description="页码"),
  582. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  583. status: Optional[str] = Query(None, description="状态"),
  584. db: Session = Depends(get_db),
  585. current_user: dict = Depends(require_supplier)
  586. ):
  587. """获取供应商的价格清单列表"""
  588. from app.crud.price_list import price_list_crud
  589. from app.crud.region import region_crud
  590. user = user_crud.get(db, current_user.get("user_id"))
  591. if not user:
  592. return error_response(404, "用户不存在")
  593. price_lists, total = price_list_crud.get_multi_with_filters(
  594. db,
  595. supplier_id=user.id,
  596. status=status,
  597. skip=(page - 1) * pageSize,
  598. limit=pageSize
  599. )
  600. region_map = {}
  601. for pl in price_lists:
  602. if pl.region_id and pl.region_id not in region_map:
  603. region = region_crud.get(db, pl.region_id)
  604. region_map[pl.region_id] = region.name if region else None
  605. return success_response({
  606. "list": [{
  607. "id": pl.id,
  608. "regionId": pl.region_id,
  609. "regionName": region_map.get(pl.region_id),
  610. "version": pl.version,
  611. "status": pl.status,
  612. "effectiveTime": str(pl.effective_time) if pl.effective_time else None,
  613. "publishedTime": str(pl.published_time) if pl.published_time else None,
  614. "remark": pl.remark,
  615. "createAt": str(pl.create_at) if pl.create_at else None
  616. } for pl in price_lists],
  617. "total": total,
  618. "page": page,
  619. "pageSize": pageSize,
  620. "totalPages": (total + pageSize - 1) // pageSize
  621. })
  622. @router.get("/price-lists/{price_list_id}", response_model=dict)
  623. def get_my_price_list_detail(
  624. price_list_id: str,
  625. db: Session = Depends(get_db),
  626. current_user: dict = Depends(require_supplier)
  627. ):
  628. """获取供应商价格清单详情"""
  629. from app.crud.price_list import price_list_crud, price_list_item_crud
  630. from app.crud.region import region_crud
  631. from app.crud.product import product_crud
  632. from app.crud.unit import unit_crud
  633. user = user_crud.get(db, current_user.get("user_id"))
  634. if not user:
  635. return error_response(404, "用户不存在")
  636. price_list = price_list_crud.get(db, price_list_id)
  637. if not price_list:
  638. return error_response(404, "价格清单不存在")
  639. if price_list.supplier_id != user.id:
  640. return error_response(403, "无权查看此价格清单")
  641. items = price_list_item_crud.get_by_price_list(db, price_list_id)
  642. # 获取关联信息
  643. region = region_crud.get(db, price_list.region_id)
  644. region_name = region.name if region else None
  645. item_details = []
  646. for item in items:
  647. product = product_crud.get(db, item.product_id)
  648. category_name = None
  649. if product and product.category_id:
  650. category = category_crud.get(db, product.category_id)
  651. category_name = category.name if category else None
  652. unit_name = None
  653. if item.unit_id:
  654. u = unit_crud.get(db, item.unit_id)
  655. unit_name = u.name if u else None
  656. item_details.append({
  657. "productId": item.product_id,
  658. "productName": product.name if product else None,
  659. "categoryName": category_name,
  660. "unitId": item.unit_id,
  661. "unitName": unit_name,
  662. "price": float(item.price) if item.price else 0,
  663. "imageUrl": product.image_url if product else None
  664. })
  665. return success_response({
  666. "id": price_list.id,
  667. "regionId": price_list.region_id,
  668. "regionName": region_name,
  669. "version": price_list.version,
  670. "status": price_list.status,
  671. "effectiveTime": str(price_list.effective_time) if price_list.effective_time else None,
  672. "publishedTime": str(price_list.published_time) if price_list.published_time else None,
  673. "remark": price_list.remark,
  674. "createAt": str(price_list.create_at) if price_list.create_at else None,
  675. "items": item_details
  676. })
  677. # ============ 配送任务管理 ============
  678. @router.get("/tasks", response_model=dict)
  679. def list_supplier_tasks(
  680. page: int = Query(1, ge=1, description="页码"),
  681. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  682. supplierPrepareStatus: Optional[str] = Query(
  683. None,
  684. description="供应商备货状态 supplier_prepare_status;逗号多选,如 stocking,prepared",
  685. ),
  686. status: Optional[str] = Query(
  687. None,
  688. description="兼容:任务主状态 status;未传 supplierPrepareStatus 时生效",
  689. ),
  690. keyword: Optional[str] = Query(None, description="检索:配送单号、订单号、联系人、电话"),
  691. db: Session = Depends(get_db),
  692. current_user: dict = Depends(require_supplier)
  693. ):
  694. """获取供应商的配送任务列表(含商品缩略列表;供货合计按价格清单还原供货单价后汇总)"""
  695. from sqlalchemy import and_
  696. from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
  697. from app.crud.order import order_crud, order_item_crud
  698. from app.crud.product import product_crud
  699. from app.crud.unit import unit_crud
  700. from app.models.models import BizPriceListItem
  701. user = user_crud.get(db, current_user.get("user_id"))
  702. if not user:
  703. return error_response(404, "用户不存在")
  704. tasks, total = delivery_task_crud.get_multi_with_filters(
  705. db,
  706. supplier_id=user.id,
  707. supplier_prepare_status=supplierPrepareStatus,
  708. status=None if supplierPrepareStatus else status,
  709. keyword=keyword,
  710. skip=(page - 1) * pageSize,
  711. limit=pageSize
  712. )
  713. result = []
  714. for task in tasks:
  715. region_name = None
  716. if task.region_id:
  717. region = region_crud.get(db, task.region_id)
  718. region_name = region.name if region else None
  719. # 任务行 + 供货小计(与任务详情中价格清单逻辑一致)
  720. task_items = delivery_task_item_crud.get_by_task(db, task.id)
  721. total_items = len(task_items)
  722. delivered_items = len([i for i in task_items if i.status == "delivered"])
  723. order = order_crud.get(db, task.order_id) if task.order_id else None
  724. line_items = []
  725. total_quantity = 0.0
  726. supplier_quote_total = 0.0
  727. for ti in task_items:
  728. product = product_crud.get(db, ti.product_id)
  729. unit_name = None
  730. supplier_quote_line = None
  731. oi = None
  732. if ti.order_item_id:
  733. oi = order_item_crud.get(db, ti.order_item_id)
  734. if oi and oi.unit_id:
  735. u = unit_crud.get(db, oi.unit_id)
  736. unit_name = u.name if u else None
  737. if order and oi:
  738. pli = db.query(BizPriceListItem).filter(
  739. and_(
  740. BizPriceListItem.price_list_id == order.price_list_id,
  741. BizPriceListItem.product_id == oi.product_id,
  742. BizPriceListItem.unit_id == oi.unit_id,
  743. BizPriceListItem.is_del == 0,
  744. )
  745. ).first()
  746. if pli is not None and pli.price is not None:
  747. unit_price = _supplier_quote_unit_yuan_from_pli(pli)
  748. if unit_price is not None:
  749. qty = float(ti.quantity) if ti.quantity else 0.0
  750. supplier_quote_line = round(qty * unit_price, 2)
  751. supplier_quote_total += supplier_quote_line
  752. qty = float(ti.quantity) if ti.quantity else 0.0
  753. total_quantity += qty
  754. line_items.append({
  755. "id": ti.id,
  756. "productId": ti.product_id,
  757. "productName": product.name if product else None,
  758. "quantity": qty,
  759. "unitName": unit_name,
  760. "imageUrl": product.image_url if product else None,
  761. })
  762. # 获取配送员信息(列表不返回客户信息,仅展示配送员对接用)
  763. deliver_name = None
  764. deliver_phone = None
  765. if task.deliver_id:
  766. deliver = user_crud.get(db, task.deliver_id)
  767. if deliver:
  768. deliver_name = deliver.real_name
  769. deliver_phone = deliver.phone
  770. result.append({
  771. "id": task.id,
  772. "taskNo": task.task_no,
  773. "orderId": task.order_id,
  774. "regionId": task.region_id,
  775. "regionName": region_name,
  776. "supplierId": task.supplier_id,
  777. "deliverId": task.deliver_id,
  778. "deliverName": deliver_name,
  779. "deliverPhone": deliver_phone,
  780. "status": task.status,
  781. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  782. "effectiveTime": str(task.effective_time) if task.effective_time else None,
  783. "pickupTime": str(task.pickup_time) if task.pickup_time else None,
  784. "pickupRemark": task.pickup_remark,
  785. "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
  786. "totalItems": total_items,
  787. "deliveredItems": delivered_items,
  788. "totalQuantity": round(total_quantity, 3),
  789. "supplierQuoteTotal": round(supplier_quote_total, 2),
  790. "items": line_items,
  791. "createAt": str(task.create_at) if task.create_at else None
  792. })
  793. return success_response({
  794. "list": result,
  795. "total": total,
  796. "page": page,
  797. "pageSize": pageSize,
  798. "totalPages": (total + pageSize - 1) // pageSize
  799. })
  800. @router.get("/tasks/{task_id}", response_model=dict)
  801. def get_supplier_task_detail(
  802. task_id: str,
  803. db: Session = Depends(get_db),
  804. current_user: dict = Depends(require_supplier)
  805. ):
  806. """获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供货价)"""
  807. from sqlalchemy import and_
  808. from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
  809. from app.crud.order import order_crud, order_item_crud
  810. from app.crud.product import product_crud
  811. from app.crud.unit import unit_crud
  812. from app.models.models import BizPriceListItem
  813. user = user_crud.get(db, current_user.get("user_id"))
  814. if not user:
  815. return error_response(404, "用户不存在")
  816. task = delivery_task_crud.get(db, task_id)
  817. if not task:
  818. return error_response(404, "任务不存在")
  819. if task.supplier_id != user.id:
  820. return error_response(403, "无权查看此任务")
  821. order = None
  822. order_info = None
  823. if task.order_id:
  824. order = order_crud.get(db, task.order_id)
  825. if order:
  826. # 供应商备货详情仅展示客户订单号,不下发客户联系人、地址等
  827. order_info = {
  828. "orderNo": order.order_no,
  829. }
  830. deliver_info = None
  831. if task.deliver_id:
  832. deliver = user_crud.get(db, task.deliver_id)
  833. if deliver:
  834. deliver_info = {
  835. "deliverId": deliver.id,
  836. "deliverName": deliver.real_name,
  837. "phone": deliver.phone
  838. }
  839. task_items = delivery_task_item_crud.get_by_task(db, task_id)
  840. item_details = []
  841. for item in task_items:
  842. product = product_crud.get(db, item.product_id)
  843. unit_name = None
  844. supplier_quote_unit = None
  845. supplier_quote_line = None
  846. product_image_url = None
  847. product_measure_value = None
  848. product_unit_name = None
  849. if product:
  850. product_image_url = product.image_url
  851. if product.measure_value is not None:
  852. product_measure_value = float(product.measure_value)
  853. if product.unit_id:
  854. pu = unit_crud.get(db, product.unit_id)
  855. product_unit_name = pu.name if pu else None
  856. oi = None
  857. if item.order_item_id:
  858. oi = order_item_crud.get(db, item.order_item_id)
  859. if oi and oi.unit_id:
  860. u = unit_crud.get(db, oi.unit_id)
  861. unit_name = u.name if u else None
  862. if order and oi:
  863. pli = db.query(BizPriceListItem).filter(
  864. and_(
  865. BizPriceListItem.price_list_id == order.price_list_id,
  866. BizPriceListItem.product_id == oi.product_id,
  867. BizPriceListItem.unit_id == oi.unit_id,
  868. BizPriceListItem.is_del == 0,
  869. )
  870. ).first()
  871. if pli is not None and pli.price is not None:
  872. supplier_quote_unit = _supplier_quote_unit_yuan_from_pli(pli)
  873. if supplier_quote_unit is not None:
  874. qty = float(item.quantity) if item.quantity else 0.0
  875. supplier_quote_line = round(qty * supplier_quote_unit, 2)
  876. item_details.append({
  877. "id": item.id,
  878. "orderItemId": item.order_item_id,
  879. "productId": item.product_id,
  880. "productName": product.name if product else None,
  881. "imageUrl": product_image_url,
  882. "measureValue": product_measure_value,
  883. "productUnitName": product_unit_name,
  884. "unitName": unit_name,
  885. "quantity": float(item.quantity) if item.quantity else 0,
  886. "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0,
  887. "supplierQuoteUnit": supplier_quote_unit,
  888. "supplierQuoteLine": supplier_quote_line,
  889. "status": item.status,
  890. "deliveryTime": str(item.delivery_time) if item.delivery_time else None
  891. })
  892. prepared_images = [
  893. a.file_url
  894. for a in attachment_crud.get_by_business(db, "delivery_prepare", task_id)
  895. if getattr(a, "file_url", None)
  896. ]
  897. if (not prepared_images) and getattr(task, "pickup_image_url", None):
  898. prepared_images = [s.strip() for s in str(task.pickup_image_url).split(",") if s and str(s).strip()]
  899. return success_response({
  900. "id": task.id,
  901. "taskNo": task.task_no,
  902. "orderId": task.order_id,
  903. "regionId": task.region_id,
  904. "status": task.status,
  905. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  906. "effectiveTime": str(task.effective_time) if task.effective_time else None,
  907. "pickupTime": str(task.pickup_time) if task.pickup_time else None,
  908. "pickupRemark": task.pickup_remark,
  909. "pickupImageUrl": task.pickup_image_url,
  910. "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
  911. "deliveryRemark": task.delivery_remark,
  912. "deliveryImageUrl": task.delivery_image_url,
  913. "preparedImages": prepared_images,
  914. "order": order_info,
  915. "deliver": deliver_info,
  916. "items": item_details,
  917. "createAt": str(task.create_at) if task.create_at else None
  918. })
  919. def _notify_deliver_stock_ready(db, task, user, remark: str = None) -> bool:
  920. """备货完成后通知配送员(微信订阅消息,失败不阻断主流程)"""
  921. if not task.deliver_id:
  922. return False
  923. try:
  924. from app.core.wechat_message import WechatMessageService
  925. deliver = user_crud.get(db, task.deliver_id)
  926. if deliver and hasattr(deliver, "openid"):
  927. WechatMessageService.send_delivery_notice(
  928. openid=deliver.openid,
  929. task_no=task.task_no,
  930. supplier_name=user.real_name or "供应商",
  931. address="商品已备好,请尽快取货",
  932. remark=remark or "供应商已备货完成,请及时取货",
  933. )
  934. return True
  935. except Exception:
  936. pass
  937. return False
  938. @router.post("/tasks/{task_id}/start-stock", response_model=dict)
  939. def supplier_start_stock(
  940. task_id: str,
  941. request: dict,
  942. db: Session = Depends(get_db),
  943. current_user: dict = Depends(require_supplier),
  944. ):
  945. """供应商开始备货:assigned -> supplier_stocking,并写入订单状态日志(配送进度)"""
  946. from app.crud.delivery import delivery_task_crud
  947. user = user_crud.get(db, current_user.get("user_id"))
  948. if not user:
  949. return error_response(404, "用户不存在")
  950. task = delivery_task_crud.get(db, task_id)
  951. if not task:
  952. return error_response(404, "任务不存在")
  953. if task.supplier_id != user.id:
  954. return error_response(403, "无权操作此任务")
  955. if (getattr(task, "supplier_prepare_status", None) or "pending_prepare") != "pending_prepare":
  956. return error_response(400, "当前状态不可开始备货(需为待备货/已分配)")
  957. remark = (request.get("remark") or "").strip() or None
  958. task = delivery_task_crud.update_status(
  959. db,
  960. task_id=task_id,
  961. new_status="supplier_stocking",
  962. operator_id=user.id,
  963. remark=remark,
  964. operator_real_name=user.real_name,
  965. operator_role="supplier",
  966. )
  967. if not task:
  968. return error_response(400, "操作失败")
  969. return success_response({
  970. "id": task.id,
  971. "taskNo": task.task_no,
  972. "status": task.status,
  973. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  974. "delivererStatus": getattr(task, "deliverer_status", None),
  975. "message": "已开始备货",
  976. })
  977. @router.post("/tasks/{task_id}/finish-stock", response_model=dict)
  978. def supplier_finish_stock(
  979. task_id: str,
  980. request: dict,
  981. db: Session = Depends(get_db),
  982. current_user: dict = Depends(require_supplier),
  983. ):
  984. """供应商备货完成:supplier_stocking -> supplier_ready(待配送员取货),写入订单日志并可通知配送员"""
  985. from app.crud.delivery import delivery_task_crud
  986. user = user_crud.get(db, current_user.get("user_id"))
  987. if not user:
  988. return error_response(404, "用户不存在")
  989. task = delivery_task_crud.get(db, task_id)
  990. if not task:
  991. return error_response(404, "任务不存在")
  992. if task.supplier_id != user.id:
  993. return error_response(403, "无权操作此任务")
  994. ss = getattr(task, "supplier_prepare_status", None) or "pending_prepare"
  995. if ss == "pending_prepare":
  996. return error_response(400, "请先开始备货后再确认备货完成")
  997. if ss not in ("stocking", "prepared"):
  998. return error_response(400, "当前状态不可确认备货完成")
  999. if ss == "prepared":
  1000. ds = (getattr(task, "deliverer_status", None) or "").strip().lower()
  1001. if ds != "pending_pickup":
  1002. return error_response(400, "配送员已取货,备货凭证不可再修改")
  1003. remark = (request.get("remark") or request.get("preparedRemark") or "").strip() or None
  1004. image_urls = _normalize_image_urls(request.get("imageUrls"))
  1005. if not image_urls:
  1006. prep_img = request.get("preparedImageUrl") or request.get("pickupImageUrl")
  1007. image_urls = _normalize_image_urls(prep_img)
  1008. if not image_urls:
  1009. return error_response(400, "请上传备货完成凭证照片")
  1010. if ss == "stocking":
  1011. task = delivery_task_crud.update_status(
  1012. db,
  1013. task_id=task_id,
  1014. new_status="supplier_ready",
  1015. operator_id=user.id,
  1016. remark=remark,
  1017. image_urls=image_urls,
  1018. operator_real_name=user.real_name,
  1019. operator_role="supplier",
  1020. )
  1021. if not task:
  1022. return error_response(400, "操作失败")
  1023. else:
  1024. task = delivery_task_crud.get(db, task_id)
  1025. if not task:
  1026. return error_response(404, "任务不存在")
  1027. _replace_delivery_prepare_attachments(
  1028. db,
  1029. task_id=task_id,
  1030. image_urls=image_urls,
  1031. uploader_id=user.id,
  1032. uploader_name=user.real_name,
  1033. )
  1034. db.commit()
  1035. deliver_notified = _notify_deliver_stock_ready(db, task, user, remark) if ss == "stocking" else False
  1036. return success_response({
  1037. "id": task.id,
  1038. "taskNo": task.task_no,
  1039. "status": task.status,
  1040. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  1041. "delivererStatus": getattr(task, "deliverer_status", None),
  1042. "message": ("已确认备货完成" if ss == "stocking" else "备货凭证已更新")
  1043. + (",已通知配送员" if deliver_notified else ""),
  1044. })
  1045. # 兼容旧路径:与 start-stock / finish-stock 行为一致
  1046. @router.post("/tasks/{task_id}/confirm-pickup", response_model=dict)
  1047. def supplier_confirm_pickup_deprecated(
  1048. task_id: str,
  1049. request: dict,
  1050. db: Session = Depends(get_db),
  1051. current_user: dict = Depends(require_supplier),
  1052. ):
  1053. """[兼容] 请改用 POST /tasks/{id}/start-stock"""
  1054. return supplier_start_stock(task_id, request, db, current_user)
  1055. @router.post("/tasks/{task_id}/confirm-prepared", response_model=dict)
  1056. def supplier_confirm_prepared_deprecated(
  1057. task_id: str,
  1058. request: dict,
  1059. db: Session = Depends(get_db),
  1060. current_user: dict = Depends(require_supplier),
  1061. ):
  1062. """[兼容] 请改用 POST /tasks/{id}/finish-stock"""
  1063. return supplier_finish_stock(task_id, request, db, current_user)
  1064. @router.get("/dashboard/statistics", response_model=dict)
  1065. def supplier_dashboard_statistics(
  1066. db: Session = Depends(get_db),
  1067. current_user: dict = Depends(require_supplier)
  1068. ):
  1069. """供应商首页统计(询价/报价/任务概览)"""
  1070. from app.crud.delivery import delivery_task_crud
  1071. from app.models.models import BizInquiry, BizQuotation
  1072. from sqlalchemy import and_
  1073. uid = current_user.get("user_id")
  1074. user = user_crud.get(db, uid)
  1075. rid = user.region_id if user else None
  1076. q_inq = db.query(BizInquiry).filter(
  1077. and_(BizInquiry.is_del == 0, BizInquiry.status == "published")
  1078. )
  1079. if rid:
  1080. q_inq = q_inq.filter(BizInquiry.region_id == rid)
  1081. pending_inq = q_inq.count()
  1082. my_quo = db.query(BizQuotation).filter(
  1083. and_(BizQuotation.is_del == 0, BizQuotation.supplier_id == uid)
  1084. ).count()
  1085. pending_tasks, _ = delivery_task_crud.get_by_supplier(
  1086. db, supplier_id=uid, status="assigned", skip=0, limit=500
  1087. )
  1088. stocking, _ = delivery_task_crud.get_by_supplier(
  1089. db, supplier_id=uid, status="supplier_stocking", skip=0, limit=500
  1090. )
  1091. return success_response({
  1092. "pendingInquiries": pending_inq,
  1093. "myQuotations": my_quo,
  1094. "pendingTasks": len(pending_tasks) + len(stocking),
  1095. "todayDeliveries": 0,
  1096. })
  1097. @router.get("/products/{product_id}", response_model=dict)
  1098. def get_supplier_product_detail(
  1099. product_id: str,
  1100. db: Session = Depends(get_db),
  1101. current_user: dict = Depends(require_supplier)
  1102. ):
  1103. """供应商端商品详情:轮播、基础信息、参数、富文本、详情媒体"""
  1104. from app.crud.product import product_crud
  1105. from app.crud.unit import unit_crud
  1106. from app.crud.product_banner import product_banner
  1107. from app.crud.product_parameter_value import product_parameter_value
  1108. from app.crud.product_detail_media import product_detail_media
  1109. product = product_crud.get(db, product_id)
  1110. if not product or getattr(product, "is_del", 0) == 1:
  1111. return error_response(404, "商品不存在")
  1112. unit = unit_crud.get(db, product.unit_id) if product.unit_id else None
  1113. banners_raw = product_banner.get_by_product(db, product_id)
  1114. banners = [{
  1115. "mediaType": b.media_type,
  1116. "fileUrl": b.file_url,
  1117. "thumbnailUrl": b.thumbnail_url,
  1118. "sortOrder": b.sort_order,
  1119. } for b in banners_raw]
  1120. pv_rows = product_parameter_value.get_by_product_with_detail(db, product_id)
  1121. parameter_values = []
  1122. for row in pv_rows:
  1123. val = row.get("param_value")
  1124. if val is None or str(val).strip() == "":
  1125. continue
  1126. parameter_values.append({
  1127. "parameterId": row.get("parameter_id"),
  1128. "parameterName": row.get("parameter_name") or "",
  1129. "paramValue": str(val),
  1130. })
  1131. media_raw = product_detail_media.get_by_product(db, product_id)
  1132. detail_media = []
  1133. for m in media_raw:
  1134. if m.content_type not in ("image", "video"):
  1135. continue
  1136. if not m.file_url:
  1137. continue
  1138. detail_media.append({
  1139. "id": m.id,
  1140. "contentType": m.content_type,
  1141. "fileUrl": m.file_url,
  1142. "thumbnailUrl": m.thumbnail_url,
  1143. "description": m.description,
  1144. "sortOrder": m.sort_order,
  1145. })
  1146. return success_response({
  1147. "product": {
  1148. "id": product.id,
  1149. "name": product.name,
  1150. "unitId": product.unit_id,
  1151. "unitName": unit.name if unit else None,
  1152. "measureValue": float(product.measure_value) if product.measure_value is not None else None,
  1153. "detailContent": product.detail_content or "",
  1154. },
  1155. "banners": banners,
  1156. "parameterValues": parameter_values,
  1157. "detailMedia": detail_media,
  1158. })