mini_supplier.py 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345
  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 decimal import Decimal
  434. user = user_crud.get(db, current_user.get("user_id"))
  435. if not user or not user.region_id:
  436. return error_response(400, "用户未绑定区域")
  437. inquiry_id = request.get("inquiryId")
  438. items = request.get("items", [])
  439. if not inquiry_id:
  440. return error_response(400, "询价单ID不能为空")
  441. if not items:
  442. return error_response(400, "报价项不能为空")
  443. inquiry = inquiry_crud.get(db, inquiry_id)
  444. if not inquiry:
  445. return error_response(404, "询价单不存在")
  446. if inquiry.region_id != user.region_id:
  447. return error_response(403, "无权对此询价单报价")
  448. if inquiry.status != "published":
  449. return error_response(400, "询价单已关闭")
  450. # 支持同一询价单多次报价:每次提交都创建一张新报价单
  451. inquiry_items = inquiry_item_crud.get_by_inquiry(db, inquiry_id)
  452. inquiry_product_ids = set(item.product_id for item in inquiry_items)
  453. submitted_product_ids = set(item.get("productId") for item in items)
  454. if submitted_product_ids != inquiry_product_ids:
  455. return error_response(400, "报价商品必须与询价商品一致")
  456. creator = {"user_id": user.id, "real_name": user.real_name}
  457. quotation_data = {
  458. "inquiry_id": inquiry_id,
  459. "supplier_id": user.id,
  460. "region_id": user.region_id,
  461. "status": "submitted",
  462. "submit_time": datetime.now()
  463. }
  464. quotation_items_data = []
  465. total_amount = Decimal("0")
  466. for item in items:
  467. price = Decimal(str(item.get("price", 0)))
  468. total_amount += price
  469. quotation_items_data.append({
  470. "product_id": item.get("productId"),
  471. "unit_id": item.get("unitId"),
  472. "price": price
  473. })
  474. quotation_data["total_amount"] = total_amount
  475. quotation = quotation_crud.create(db, quotation_data, creator)
  476. quotation_item_crud.create_items(db, quotation.id, quotation_items_data, creator)
  477. return success_response({
  478. "id": quotation.id,
  479. "totalAmount": float(quotation.total_amount),
  480. "status": quotation.status,
  481. "submitTime": str(quotation.submit_time) if quotation.submit_time else None
  482. })
  483. @router.get("/quotations", response_model=dict)
  484. def list_my_quotations(
  485. page: int = Query(1, ge=1, description="页码"),
  486. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  487. inquiryId: Optional[str] = Query(None, description="询价单ID"),
  488. status: Optional[str] = Query(None, description="报价状态,如 selected=已采纳"),
  489. db: Session = Depends(get_db),
  490. current_user: dict = Depends(require_supplier)
  491. ):
  492. """获取我的报价列表"""
  493. from app.crud.inquiry import inquiry_crud
  494. from app.crud.quotation import quotation_crud
  495. user = user_crud.get(db, current_user.get("user_id"))
  496. if not user:
  497. return error_response(404, "用户不存在")
  498. quotations, total = quotation_crud.get_multi_with_filters(
  499. db,
  500. supplier_id=user.id,
  501. inquiry_id=inquiryId,
  502. status=status,
  503. skip=(page - 1) * pageSize,
  504. limit=pageSize
  505. )
  506. inquiry_map = {}
  507. quotation_list = []
  508. for q in quotations:
  509. if q.inquiry_id and q.inquiry_id not in inquiry_map:
  510. inquiry = inquiry_crud.get(db, q.inquiry_id)
  511. inquiry_map[q.inquiry_id] = inquiry.title if inquiry else None
  512. quotation_list.append({
  513. "id": q.id,
  514. "inquiryId": q.inquiry_id,
  515. "inquiryTitle": inquiry_map.get(q.inquiry_id),
  516. "status": q.status,
  517. "totalAmount": float(q.total_amount) if q.total_amount else 0,
  518. "submitTime": str(q.submit_time) if q.submit_time else None,
  519. })
  520. return success_response({
  521. "list": quotation_list,
  522. "total": total,
  523. "page": page,
  524. "pageSize": pageSize,
  525. "totalPages": (total + pageSize - 1) // pageSize
  526. })
  527. @router.get("/quotations/{quotation_id}", response_model=dict)
  528. def get_my_quotation(
  529. quotation_id: str,
  530. db: Session = Depends(get_db),
  531. current_user: dict = Depends(require_supplier)
  532. ):
  533. """获取我的报价单详情"""
  534. from app.crud.inquiry import inquiry_crud, inquiry_item_crud
  535. from app.crud.quotation import quotation_crud, quotation_item_crud
  536. from app.crud.product import product_crud
  537. user = user_crud.get(db, current_user.get("user_id"))
  538. if not user:
  539. return error_response(404, "用户不存在")
  540. quotation = quotation_crud.get(db, quotation_id)
  541. if not quotation:
  542. return error_response(404, "报价单不存在")
  543. if quotation.supplier_id != user.id:
  544. return error_response(403, "无权查看此报价单")
  545. items = quotation_item_crud.get_by_quotation(db, quotation_id)
  546. inquiry_title = None
  547. if quotation.inquiry_id:
  548. inquiry = inquiry_crud.get(db, quotation.inquiry_id)
  549. inquiry_title = inquiry.title if inquiry else None
  550. item_details = []
  551. for item in items:
  552. product = product_crud.get(db, item.product_id)
  553. item_details.append({
  554. "id": item.id,
  555. "productId": item.product_id,
  556. "productName": product.name if product else None,
  557. "unitId": item.unit_id,
  558. "price": float(item.price) if item.price else 0,
  559. "createAt": str(item.create_at) if item.create_at else None
  560. })
  561. return success_response({
  562. "id": quotation.id,
  563. "inquiryId": quotation.inquiry_id,
  564. "inquiryTitle": inquiry_title,
  565. "status": quotation.status,
  566. "totalAmount": float(quotation.total_amount) if quotation.total_amount else 0,
  567. "submitTime": str(quotation.submit_time) if quotation.submit_time else None,
  568. "items": item_details
  569. })
  570. # ============ 价格清单 - 供应商查看 ============
  571. @router.get("/price-lists", response_model=dict)
  572. def list_my_price_lists(
  573. page: int = Query(1, ge=1, description="页码"),
  574. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  575. status: Optional[str] = Query(None, description="状态"),
  576. db: Session = Depends(get_db),
  577. current_user: dict = Depends(require_supplier)
  578. ):
  579. """获取供应商的价格清单列表"""
  580. from app.crud.price_list import price_list_crud
  581. from app.crud.region import region_crud
  582. user = user_crud.get(db, current_user.get("user_id"))
  583. if not user:
  584. return error_response(404, "用户不存在")
  585. price_lists, total = price_list_crud.get_multi_with_filters(
  586. db,
  587. supplier_id=user.id,
  588. status=status,
  589. skip=(page - 1) * pageSize,
  590. limit=pageSize
  591. )
  592. region_map = {}
  593. for pl in price_lists:
  594. if pl.region_id and pl.region_id not in region_map:
  595. region = region_crud.get(db, pl.region_id)
  596. region_map[pl.region_id] = region.name if region else None
  597. return success_response({
  598. "list": [{
  599. "id": pl.id,
  600. "regionId": pl.region_id,
  601. "regionName": region_map.get(pl.region_id),
  602. "version": pl.version,
  603. "status": pl.status,
  604. "effectiveTime": str(pl.effective_time) if pl.effective_time else None,
  605. "publishedTime": str(pl.published_time) if pl.published_time else None,
  606. "remark": pl.remark,
  607. "createAt": str(pl.create_at) if pl.create_at else None
  608. } for pl in price_lists],
  609. "total": total,
  610. "page": page,
  611. "pageSize": pageSize,
  612. "totalPages": (total + pageSize - 1) // pageSize
  613. })
  614. @router.get("/price-lists/{price_list_id}", response_model=dict)
  615. def get_my_price_list_detail(
  616. price_list_id: str,
  617. db: Session = Depends(get_db),
  618. current_user: dict = Depends(require_supplier)
  619. ):
  620. """获取供应商价格清单详情"""
  621. from app.crud.price_list import price_list_crud, price_list_item_crud
  622. from app.crud.region import region_crud
  623. from app.crud.product import product_crud
  624. from app.crud.unit import unit_crud
  625. user = user_crud.get(db, current_user.get("user_id"))
  626. if not user:
  627. return error_response(404, "用户不存在")
  628. price_list = price_list_crud.get(db, price_list_id)
  629. if not price_list:
  630. return error_response(404, "价格清单不存在")
  631. if price_list.supplier_id != user.id:
  632. return error_response(403, "无权查看此价格清单")
  633. items = price_list_item_crud.get_by_price_list(db, price_list_id)
  634. # 获取关联信息
  635. region = region_crud.get(db, price_list.region_id)
  636. region_name = region.name if region else None
  637. item_details = []
  638. for item in items:
  639. product = product_crud.get(db, item.product_id)
  640. category_name = None
  641. if product and product.category_id:
  642. category = category_crud.get(db, product.category_id)
  643. category_name = category.name if category else None
  644. unit_name = None
  645. if item.unit_id:
  646. u = unit_crud.get(db, item.unit_id)
  647. unit_name = u.name if u else None
  648. item_details.append({
  649. "productId": item.product_id,
  650. "productName": product.name if product else None,
  651. "categoryName": category_name,
  652. "unitId": item.unit_id,
  653. "unitName": unit_name,
  654. "price": float(item.price) if item.price else 0,
  655. "imageUrl": product.image_url if product else None
  656. })
  657. return success_response({
  658. "id": price_list.id,
  659. "regionId": price_list.region_id,
  660. "regionName": region_name,
  661. "version": price_list.version,
  662. "status": price_list.status,
  663. "effectiveTime": str(price_list.effective_time) if price_list.effective_time else None,
  664. "publishedTime": str(price_list.published_time) if price_list.published_time else None,
  665. "remark": price_list.remark,
  666. "createAt": str(price_list.create_at) if price_list.create_at else None,
  667. "items": item_details
  668. })
  669. # ============ 配送任务管理 ============
  670. @router.get("/tasks", response_model=dict)
  671. def list_supplier_tasks(
  672. page: int = Query(1, ge=1, description="页码"),
  673. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  674. supplierPrepareStatus: Optional[str] = Query(
  675. None,
  676. description="供应商备货状态 supplier_prepare_status;逗号多选,如 stocking,prepared",
  677. ),
  678. status: Optional[str] = Query(
  679. None,
  680. description="兼容:任务主状态 status;未传 supplierPrepareStatus 时生效",
  681. ),
  682. keyword: Optional[str] = Query(None, description="检索:配送单号、订单号、联系人、电话"),
  683. db: Session = Depends(get_db),
  684. current_user: dict = Depends(require_supplier)
  685. ):
  686. """获取供应商的配送任务列表(含商品缩略列表;供货合计按价格清单还原供货单价后汇总)"""
  687. from sqlalchemy import and_
  688. from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
  689. from app.crud.order import order_crud, order_item_crud
  690. from app.crud.product import product_crud
  691. from app.crud.unit import unit_crud
  692. from app.models.models import BizPriceListItem
  693. user = user_crud.get(db, current_user.get("user_id"))
  694. if not user:
  695. return error_response(404, "用户不存在")
  696. tasks, total = delivery_task_crud.get_multi_with_filters(
  697. db,
  698. supplier_id=user.id,
  699. supplier_prepare_status=supplierPrepareStatus,
  700. status=None if supplierPrepareStatus else status,
  701. keyword=keyword,
  702. skip=(page - 1) * pageSize,
  703. limit=pageSize
  704. )
  705. result = []
  706. for task in tasks:
  707. region_name = None
  708. if task.region_id:
  709. region = region_crud.get(db, task.region_id)
  710. region_name = region.name if region else None
  711. # 任务行 + 供货小计(与任务详情中价格清单逻辑一致)
  712. task_items = delivery_task_item_crud.get_by_task(db, task.id)
  713. total_items = len(task_items)
  714. delivered_items = len([i for i in task_items if i.status == "delivered"])
  715. order = order_crud.get(db, task.order_id) if task.order_id else None
  716. line_items = []
  717. total_quantity = 0.0
  718. supplier_quote_total = 0.0
  719. for ti in task_items:
  720. product = product_crud.get(db, ti.product_id)
  721. unit_name = None
  722. supplier_quote_line = None
  723. oi = None
  724. if ti.order_item_id:
  725. oi = order_item_crud.get(db, ti.order_item_id)
  726. if oi and oi.unit_id:
  727. u = unit_crud.get(db, oi.unit_id)
  728. unit_name = u.name if u else None
  729. if order and oi:
  730. pli = db.query(BizPriceListItem).filter(
  731. and_(
  732. BizPriceListItem.price_list_id == order.price_list_id,
  733. BizPriceListItem.product_id == oi.product_id,
  734. BizPriceListItem.unit_id == oi.unit_id,
  735. BizPriceListItem.is_del == 0,
  736. )
  737. ).first()
  738. if pli is not None and pli.price is not None:
  739. unit_price = _supplier_quote_unit_yuan_from_pli(pli)
  740. if unit_price is not None:
  741. qty = float(ti.quantity) if ti.quantity else 0.0
  742. supplier_quote_line = round(qty * unit_price, 2)
  743. supplier_quote_total += supplier_quote_line
  744. qty = float(ti.quantity) if ti.quantity else 0.0
  745. total_quantity += qty
  746. line_items.append({
  747. "id": ti.id,
  748. "productId": ti.product_id,
  749. "productName": product.name if product else None,
  750. "quantity": qty,
  751. "unitName": unit_name,
  752. "imageUrl": product.image_url if product else None,
  753. })
  754. # 获取配送员信息(列表不返回客户信息,仅展示配送员对接用)
  755. deliver_name = None
  756. deliver_phone = None
  757. if task.deliver_id:
  758. deliver = user_crud.get(db, task.deliver_id)
  759. if deliver:
  760. deliver_name = deliver.real_name
  761. deliver_phone = deliver.phone
  762. result.append({
  763. "id": task.id,
  764. "taskNo": task.task_no,
  765. "orderId": task.order_id,
  766. "regionId": task.region_id,
  767. "regionName": region_name,
  768. "supplierId": task.supplier_id,
  769. "deliverId": task.deliver_id,
  770. "deliverName": deliver_name,
  771. "deliverPhone": deliver_phone,
  772. "status": task.status,
  773. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  774. "effectiveTime": str(task.effective_time) if task.effective_time else None,
  775. "pickupTime": str(task.pickup_time) if task.pickup_time else None,
  776. "pickupRemark": task.pickup_remark,
  777. "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
  778. "totalItems": total_items,
  779. "deliveredItems": delivered_items,
  780. "totalQuantity": round(total_quantity, 3),
  781. "supplierQuoteTotal": round(supplier_quote_total, 2),
  782. "items": line_items,
  783. "createAt": str(task.create_at) if task.create_at else None
  784. })
  785. return success_response({
  786. "list": result,
  787. "total": total,
  788. "page": page,
  789. "pageSize": pageSize,
  790. "totalPages": (total + pageSize - 1) // pageSize
  791. })
  792. @router.get("/tasks/{task_id}", response_model=dict)
  793. def get_supplier_task_detail(
  794. task_id: str,
  795. db: Session = Depends(get_db),
  796. current_user: dict = Depends(require_supplier)
  797. ):
  798. """获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供货价)"""
  799. from sqlalchemy import and_
  800. from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
  801. from app.crud.order import order_crud, order_item_crud
  802. from app.crud.product import product_crud
  803. from app.crud.unit import unit_crud
  804. from app.models.models import BizPriceListItem
  805. user = user_crud.get(db, current_user.get("user_id"))
  806. if not user:
  807. return error_response(404, "用户不存在")
  808. task = delivery_task_crud.get(db, task_id)
  809. if not task:
  810. return error_response(404, "任务不存在")
  811. if task.supplier_id != user.id:
  812. return error_response(403, "无权查看此任务")
  813. order = None
  814. order_info = None
  815. if task.order_id:
  816. order = order_crud.get(db, task.order_id)
  817. if order:
  818. # 供应商备货详情仅展示客户订单号,不下发客户联系人、地址等
  819. order_info = {
  820. "orderNo": order.order_no,
  821. }
  822. deliver_info = None
  823. if task.deliver_id:
  824. deliver = user_crud.get(db, task.deliver_id)
  825. if deliver:
  826. deliver_info = {
  827. "deliverId": deliver.id,
  828. "deliverName": deliver.real_name,
  829. "phone": deliver.phone
  830. }
  831. task_items = delivery_task_item_crud.get_by_task(db, task_id)
  832. item_details = []
  833. for item in task_items:
  834. product = product_crud.get(db, item.product_id)
  835. unit_name = None
  836. supplier_quote_unit = None
  837. supplier_quote_line = None
  838. product_image_url = None
  839. product_measure_value = None
  840. product_unit_name = None
  841. if product:
  842. product_image_url = product.image_url
  843. if product.measure_value is not None:
  844. product_measure_value = float(product.measure_value)
  845. if product.unit_id:
  846. pu = unit_crud.get(db, product.unit_id)
  847. product_unit_name = pu.name if pu else None
  848. oi = None
  849. if item.order_item_id:
  850. oi = order_item_crud.get(db, item.order_item_id)
  851. if oi and oi.unit_id:
  852. u = unit_crud.get(db, oi.unit_id)
  853. unit_name = u.name if u else None
  854. if order and oi:
  855. pli = db.query(BizPriceListItem).filter(
  856. and_(
  857. BizPriceListItem.price_list_id == order.price_list_id,
  858. BizPriceListItem.product_id == oi.product_id,
  859. BizPriceListItem.unit_id == oi.unit_id,
  860. BizPriceListItem.is_del == 0,
  861. )
  862. ).first()
  863. if pli is not None and pli.price is not None:
  864. supplier_quote_unit = _supplier_quote_unit_yuan_from_pli(pli)
  865. if supplier_quote_unit is not None:
  866. qty = float(item.quantity) if item.quantity else 0.0
  867. supplier_quote_line = round(qty * supplier_quote_unit, 2)
  868. item_details.append({
  869. "id": item.id,
  870. "orderItemId": item.order_item_id,
  871. "productId": item.product_id,
  872. "productName": product.name if product else None,
  873. "imageUrl": product_image_url,
  874. "measureValue": product_measure_value,
  875. "productUnitName": product_unit_name,
  876. "unitName": unit_name,
  877. "quantity": float(item.quantity) if item.quantity else 0,
  878. "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0,
  879. "supplierQuoteUnit": supplier_quote_unit,
  880. "supplierQuoteLine": supplier_quote_line,
  881. "status": item.status,
  882. "deliveryTime": str(item.delivery_time) if item.delivery_time else None
  883. })
  884. prepared_images = [
  885. a.file_url
  886. for a in attachment_crud.get_by_business(db, "delivery_prepare", task_id)
  887. if getattr(a, "file_url", None)
  888. ]
  889. if (not prepared_images) and getattr(task, "pickup_image_url", None):
  890. prepared_images = [s.strip() for s in str(task.pickup_image_url).split(",") if s and str(s).strip()]
  891. return success_response({
  892. "id": task.id,
  893. "taskNo": task.task_no,
  894. "orderId": task.order_id,
  895. "regionId": task.region_id,
  896. "status": task.status,
  897. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  898. "effectiveTime": str(task.effective_time) if task.effective_time else None,
  899. "pickupTime": str(task.pickup_time) if task.pickup_time else None,
  900. "pickupRemark": task.pickup_remark,
  901. "pickupImageUrl": task.pickup_image_url,
  902. "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
  903. "deliveryRemark": task.delivery_remark,
  904. "deliveryImageUrl": task.delivery_image_url,
  905. "preparedImages": prepared_images,
  906. "order": order_info,
  907. "deliver": deliver_info,
  908. "items": item_details,
  909. "createAt": str(task.create_at) if task.create_at else None
  910. })
  911. def _notify_deliver_stock_ready(db, task, user, remark: str = None) -> bool:
  912. """备货完成后通知配送员(微信订阅消息,失败不阻断主流程)"""
  913. if not task.deliver_id:
  914. return False
  915. try:
  916. from app.core.wechat_message import WechatMessageService
  917. deliver = user_crud.get(db, task.deliver_id)
  918. if deliver and hasattr(deliver, "openid"):
  919. WechatMessageService.send_delivery_notice(
  920. openid=deliver.openid,
  921. task_no=task.task_no,
  922. supplier_name=user.real_name or "供应商",
  923. address="商品已备好,请尽快取货",
  924. remark=remark or "供应商已备货完成,请及时取货",
  925. )
  926. return True
  927. except Exception:
  928. pass
  929. return False
  930. @router.post("/tasks/{task_id}/start-stock", response_model=dict)
  931. def supplier_start_stock(
  932. task_id: str,
  933. request: dict,
  934. db: Session = Depends(get_db),
  935. current_user: dict = Depends(require_supplier),
  936. ):
  937. """供应商开始备货:assigned -> supplier_stocking,并写入订单状态日志(配送进度)"""
  938. from app.crud.delivery import delivery_task_crud
  939. user = user_crud.get(db, current_user.get("user_id"))
  940. if not user:
  941. return error_response(404, "用户不存在")
  942. task = delivery_task_crud.get(db, task_id)
  943. if not task:
  944. return error_response(404, "任务不存在")
  945. if task.supplier_id != user.id:
  946. return error_response(403, "无权操作此任务")
  947. if (getattr(task, "supplier_prepare_status", None) or "pending_prepare") != "pending_prepare":
  948. return error_response(400, "当前状态不可开始备货(需为待备货/已分配)")
  949. remark = (request.get("remark") or "").strip() or None
  950. task = delivery_task_crud.update_status(
  951. db,
  952. task_id=task_id,
  953. new_status="supplier_stocking",
  954. operator_id=user.id,
  955. remark=remark,
  956. operator_real_name=user.real_name,
  957. operator_role="supplier",
  958. )
  959. if not task:
  960. return error_response(400, "操作失败")
  961. return success_response({
  962. "id": task.id,
  963. "taskNo": task.task_no,
  964. "status": task.status,
  965. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  966. "delivererStatus": getattr(task, "deliverer_status", None),
  967. "message": "已开始备货",
  968. })
  969. @router.post("/tasks/{task_id}/finish-stock", response_model=dict)
  970. def supplier_finish_stock(
  971. task_id: str,
  972. request: dict,
  973. db: Session = Depends(get_db),
  974. current_user: dict = Depends(require_supplier),
  975. ):
  976. """供应商备货完成:supplier_stocking -> supplier_ready(待配送员取货),写入订单日志并可通知配送员"""
  977. from app.crud.delivery import delivery_task_crud
  978. user = user_crud.get(db, current_user.get("user_id"))
  979. if not user:
  980. return error_response(404, "用户不存在")
  981. task = delivery_task_crud.get(db, task_id)
  982. if not task:
  983. return error_response(404, "任务不存在")
  984. if task.supplier_id != user.id:
  985. return error_response(403, "无权操作此任务")
  986. ss = getattr(task, "supplier_prepare_status", None) or "pending_prepare"
  987. if ss == "pending_prepare":
  988. return error_response(400, "请先开始备货后再确认备货完成")
  989. if ss not in ("stocking", "prepared"):
  990. return error_response(400, "当前状态不可确认备货完成")
  991. if ss == "prepared":
  992. ds = (getattr(task, "deliverer_status", None) or "").strip().lower()
  993. if ds != "pending_pickup":
  994. return error_response(400, "配送员已取货,备货凭证不可再修改")
  995. remark = (request.get("remark") or request.get("preparedRemark") or "").strip() or None
  996. image_urls = _normalize_image_urls(request.get("imageUrls"))
  997. if not image_urls:
  998. prep_img = request.get("preparedImageUrl") or request.get("pickupImageUrl")
  999. image_urls = _normalize_image_urls(prep_img)
  1000. if not image_urls:
  1001. return error_response(400, "请上传备货完成凭证照片")
  1002. if ss == "stocking":
  1003. task = delivery_task_crud.update_status(
  1004. db,
  1005. task_id=task_id,
  1006. new_status="supplier_ready",
  1007. operator_id=user.id,
  1008. remark=remark,
  1009. image_urls=image_urls,
  1010. operator_real_name=user.real_name,
  1011. operator_role="supplier",
  1012. )
  1013. if not task:
  1014. return error_response(400, "操作失败")
  1015. else:
  1016. task = delivery_task_crud.get(db, task_id)
  1017. if not task:
  1018. return error_response(404, "任务不存在")
  1019. _replace_delivery_prepare_attachments(
  1020. db,
  1021. task_id=task_id,
  1022. image_urls=image_urls,
  1023. uploader_id=user.id,
  1024. uploader_name=user.real_name,
  1025. )
  1026. db.commit()
  1027. deliver_notified = _notify_deliver_stock_ready(db, task, user, remark) if ss == "stocking" else False
  1028. return success_response({
  1029. "id": task.id,
  1030. "taskNo": task.task_no,
  1031. "status": task.status,
  1032. "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
  1033. "delivererStatus": getattr(task, "deliverer_status", None),
  1034. "message": ("已确认备货完成" if ss == "stocking" else "备货凭证已更新")
  1035. + (",已通知配送员" if deliver_notified else ""),
  1036. })
  1037. # 兼容旧路径:与 start-stock / finish-stock 行为一致
  1038. @router.post("/tasks/{task_id}/confirm-pickup", response_model=dict)
  1039. def supplier_confirm_pickup_deprecated(
  1040. task_id: str,
  1041. request: dict,
  1042. db: Session = Depends(get_db),
  1043. current_user: dict = Depends(require_supplier),
  1044. ):
  1045. """[兼容] 请改用 POST /tasks/{id}/start-stock"""
  1046. return supplier_start_stock(task_id, request, db, current_user)
  1047. @router.post("/tasks/{task_id}/confirm-prepared", response_model=dict)
  1048. def supplier_confirm_prepared_deprecated(
  1049. task_id: str,
  1050. request: dict,
  1051. db: Session = Depends(get_db),
  1052. current_user: dict = Depends(require_supplier),
  1053. ):
  1054. """[兼容] 请改用 POST /tasks/{id}/finish-stock"""
  1055. return supplier_finish_stock(task_id, request, db, current_user)
  1056. @router.get("/dashboard/statistics", response_model=dict)
  1057. def supplier_dashboard_statistics(
  1058. db: Session = Depends(get_db),
  1059. current_user: dict = Depends(require_supplier)
  1060. ):
  1061. """供应商首页统计(询价/报价/任务概览)"""
  1062. from app.crud.delivery import delivery_task_crud
  1063. from app.models.models import BizInquiry, BizQuotation
  1064. from sqlalchemy import and_
  1065. uid = current_user.get("user_id")
  1066. user = user_crud.get(db, uid)
  1067. rid = user.region_id if user else None
  1068. q_inq = db.query(BizInquiry).filter(
  1069. and_(BizInquiry.is_del == 0, BizInquiry.status == "published")
  1070. )
  1071. if rid:
  1072. q_inq = q_inq.filter(BizInquiry.region_id == rid)
  1073. pending_inq = q_inq.count()
  1074. my_quo = db.query(BizQuotation).filter(
  1075. and_(BizQuotation.is_del == 0, BizQuotation.supplier_id == uid)
  1076. ).count()
  1077. pending_tasks, _ = delivery_task_crud.get_by_supplier(
  1078. db, supplier_id=uid, status="assigned", skip=0, limit=500
  1079. )
  1080. stocking, _ = delivery_task_crud.get_by_supplier(
  1081. db, supplier_id=uid, status="supplier_stocking", skip=0, limit=500
  1082. )
  1083. return success_response({
  1084. "pendingInquiries": pending_inq,
  1085. "myQuotations": my_quo,
  1086. "pendingTasks": len(pending_tasks) + len(stocking),
  1087. "todayDeliveries": 0,
  1088. })
  1089. @router.get("/products/{product_id}", response_model=dict)
  1090. def get_supplier_product_detail(
  1091. product_id: str,
  1092. db: Session = Depends(get_db),
  1093. current_user: dict = Depends(require_supplier)
  1094. ):
  1095. """供应商端商品详情:轮播、基础信息、参数、富文本、详情媒体"""
  1096. from app.crud.product import product_crud
  1097. from app.crud.unit import unit_crud
  1098. from app.crud.product_banner import product_banner
  1099. from app.crud.product_parameter_value import product_parameter_value
  1100. from app.crud.product_detail_media import product_detail_media
  1101. product = product_crud.get(db, product_id)
  1102. if not product or getattr(product, "is_del", 0) == 1:
  1103. return error_response(404, "商品不存在")
  1104. unit = unit_crud.get(db, product.unit_id) if product.unit_id else None
  1105. banners_raw = product_banner.get_by_product(db, product_id)
  1106. banners = [{
  1107. "mediaType": b.media_type,
  1108. "fileUrl": b.file_url,
  1109. "thumbnailUrl": b.thumbnail_url,
  1110. "sortOrder": b.sort_order,
  1111. } for b in banners_raw]
  1112. pv_rows = product_parameter_value.get_by_product_with_detail(db, product_id)
  1113. parameter_values = []
  1114. for row in pv_rows:
  1115. val = row.get("param_value")
  1116. if val is None or str(val).strip() == "":
  1117. continue
  1118. parameter_values.append({
  1119. "parameterId": row.get("parameter_id"),
  1120. "parameterName": row.get("parameter_name") or "",
  1121. "paramValue": str(val),
  1122. })
  1123. media_raw = product_detail_media.get_by_product(db, product_id)
  1124. detail_media = []
  1125. for m in media_raw:
  1126. if m.content_type not in ("image", "video"):
  1127. continue
  1128. if not m.file_url:
  1129. continue
  1130. detail_media.append({
  1131. "id": m.id,
  1132. "contentType": m.content_type,
  1133. "fileUrl": m.file_url,
  1134. "thumbnailUrl": m.thumbnail_url,
  1135. "description": m.description,
  1136. "sortOrder": m.sort_order,
  1137. })
  1138. return success_response({
  1139. "product": {
  1140. "id": product.id,
  1141. "name": product.name,
  1142. "unitId": product.unit_id,
  1143. "unitName": unit.name if unit else None,
  1144. "measureValue": float(product.measure_value) if product.measure_value is not None else None,
  1145. "detailContent": product.detail_content or "",
  1146. },
  1147. "banners": banners,
  1148. "parameterValues": parameter_values,
  1149. "detailMedia": detail_media,
  1150. })