| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435 |
- """小程序端供应商接口"""
- from decimal import Decimal
- from typing import Optional
- from datetime import datetime
- import logging
- from fastapi import APIRouter, Depends, Request, Query
- from sqlalchemy.orm import Session
- from app.core.database import get_db
- from app.core.auth import get_current_user, get_client_ip
- from app.core.response import success_response, error_response
- from app.core.config import settings
- from app.core import api_crypto
- from app.core.request_payload import resolve_request_payload
- from app.models.models import BasAddress
- from app.crud.address import address_crud
- from app.crud.user import user_crud
- from app.crud.region import region_crud
- from app.crud.attachment import attachment_crud
- from app.models.models import BizAttachment
- router = APIRouter(prefix="/api/mini/supplier", tags=["小程序端-供应商接口"])
- logger = logging.getLogger("sqxp")
- def _supplier_quote_unit_yuan_from_pli(pli) -> Optional[float]:
- """价格清单行 price 为面向客户的最终单价;pricing_coefficient 为调价系数(%),供货单价 = price / (1 + 系数/100)。"""
- if pli is None or getattr(pli, "price", None) is None:
- return None
- final_price = Decimal(str(pli.price))
- raw_coeff = getattr(pli, "pricing_coefficient", None)
- if raw_coeff is None:
- coeff_pct = Decimal("0")
- else:
- try:
- coeff_pct = Decimal(str(raw_coeff))
- except Exception:
- coeff_pct = Decimal("0")
- factor = Decimal("1") + coeff_pct / Decimal("100")
- if factor <= Decimal("0"):
- factor = Decimal("1")
- supplier_unit = (final_price / factor).quantize(Decimal("0.01"))
- return float(supplier_unit)
- def require_supplier(current_user: dict = Depends(get_current_user)):
- """验证供应商权限"""
- if current_user.get("role") != "supplier":
- return error_response(403, "需要供应商权限")
- return current_user
- def _resolve_request_payload(request_body: dict, current_user: dict) -> dict:
- """兼容明文与密文信封,返回业务层 payload。"""
- return resolve_request_payload(request_body, current_user, source="mini_supplier")
- def _normalize_image_urls(raw) -> list[str]:
- out: list[str] = []
- if isinstance(raw, list):
- for x in raw:
- s = str(x or "").strip()
- if s:
- out.append(s)
- elif isinstance(raw, str):
- s = raw.strip()
- if s:
- out.append(s)
- return out
- def _replace_delivery_prepare_attachments(
- db: Session,
- *,
- task_id: str,
- image_urls: list[str],
- uploader_id: Optional[str],
- uploader_name: Optional[str],
- ):
- rows = db.query(BizAttachment).filter(
- BizAttachment.business_type == "delivery_prepare",
- BizAttachment.business_id == task_id,
- BizAttachment.is_del == 0,
- ).all()
- now = datetime.now()
- for r in rows:
- r.is_del = 1
- r.update_at = now
- r.update_by = uploader_id
- db.flush()
- for idx, url in enumerate(image_urls, start=1):
- attachment_crud.create_attachment(
- db=db,
- business_type="delivery_prepare",
- business_id=task_id,
- file_name=f"delivery_prepare_{idx}.jpg",
- file_url=url,
- file_type="image/jpeg",
- upload_by=uploader_id,
- upload_by_name=uploader_name,
- )
- # ============ 地址管理 ============
- @router.get("/addresses", response_model=dict)
- def list_addresses(
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取供应商取货地址列表"""
- addresses = address_crud.get_by_user(db, current_user.get("user_id"))
-
- return success_response([{
- "id": addr.id,
- "userId": addr.user_id,
- "addressType": addr.address_type,
- "contactPerson": addr.contact_person,
- "contactPhone": addr.contact_phone,
- "province": addr.province,
- "city": addr.city,
- "district": addr.district,
- "detailAddress": addr.detail_address,
- "isDefault": addr.is_default,
- "createAt": str(addr.create_at) if addr.create_at else None
- } for addr in addresses])
- @router.get("/pickup-addresses", response_model=dict)
- def list_pickup_addresses(
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取供应商取货地址列表"""
- addresses = address_crud.get_by_user_and_type(db, current_user.get("user_id"), "pickup")
-
- return success_response([{
- "id": addr.id,
- "userId": addr.user_id,
- "addressType": addr.address_type,
- "contactPerson": addr.contact_person,
- "contactPhone": addr.contact_phone,
- "province": addr.province,
- "city": addr.city,
- "district": addr.district,
- "detailAddress": addr.detail_address,
- "isDefault": addr.is_default
- } for addr in addresses])
- @router.get("/addresses/{address_id}", response_model=dict)
- def get_address(
- address_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取地址详情"""
- address = address_crud.get(db, address_id)
-
- if not address or address.user_id != current_user.get("user_id"):
- return error_response(404, "地址不存在")
-
- return success_response({
- "id": address.id,
- "userId": address.user_id,
- "addressType": address.address_type,
- "contactPerson": address.contact_person,
- "contactPhone": address.contact_phone,
- "province": address.province,
- "city": address.city,
- "district": address.district,
- "detailAddress": address.detail_address,
- "isDefault": address.is_default,
- "createAt": str(address.create_at) if address.create_at else None
- })
- @router.post("/addresses", response_model=dict)
- def create_address(
- request: dict,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """创建地址"""
- payload = _resolve_request_payload(request, current_user)
- if not payload.get("addressType") and not payload.get("address_type"):
- return error_response(400, "地址类型不能为空")
- payload["user_id"] = current_user.get("user_id")
- creator = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
-
- # 如果设置为默认,先取消其他默认
- if payload.get("isDefault"):
- address_crud.set_default(db, current_user.get("user_id"), "")
-
- address = address_crud.create(db, payload, creator)
-
- return success_response({
- "id": address.id,
- "addressType": address.address_type,
- "contactPerson": address.contact_person,
- "contactPhone": address.contact_phone
- })
- @router.put("/addresses/{address_id}", response_model=dict)
- def update_address(
- address_id: str,
- request: dict,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """更新地址"""
- address = address_crud.get(db, address_id)
-
- if not address or address.user_id != current_user.get("user_id"):
- return error_response(404, "地址不存在")
-
- payload = _resolve_request_payload(request, current_user)
- # 如果设置为默认,先取消其他默认
- if payload.get("isDefault"):
- address_crud.set_default(db, current_user.get("user_id"), address_id)
-
- updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
- address = address_crud.update_with_history(db, address_id, payload, updater)
-
- return success_response({
- "id": address.id,
- "addressType": address.address_type,
- "contactPerson": address.contact_person,
- "contactPhone": address.contact_phone
- })
- @router.delete("/addresses/{address_id}", response_model=dict)
- def delete_address(
- address_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """删除地址"""
- address = address_crud.get(db, address_id)
-
- if not address or address.user_id != current_user.get("user_id"):
- return error_response(404, "地址不存在")
-
- success = address_crud.soft_delete_with_history(db, address_id)
-
- return success_response({"id": address_id})
- @router.post("/addresses/{address_id}/set-default", response_model=dict)
- def set_default_address(
- address_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """设置默认地址"""
- address = address_crud.get(db, address_id)
-
- if not address or address.user_id != current_user.get("user_id"):
- return error_response(404, "地址不存在")
-
- success = address_crud.set_default(db, current_user.get("user_id"), address_id)
-
- if success:
- return success_response({"message": "设置成功"})
- return error_response(400, "设置失败")
- # ============ 个人资料 ============
- @router.get("/profile", response_model=dict)
- def get_profile(
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取供应商个人资料"""
- user = user_crud.get(db, current_user.get("user_id"))
-
- if not user:
- return error_response(404, "用户不存在")
-
- region_name = None
- if user.region_id:
- region = region_crud.get(db, user.region_id)
- region_name = region.name if region else None
-
- # 获取默认取货地址
- default_address = address_crud.get_default(db, user.id, "pickup")
- address_info = None
- if default_address:
- address_info = {
- "contactPerson": default_address.contact_person,
- "contactPhone": default_address.contact_phone,
- "province": default_address.province,
- "city": default_address.city,
- "district": default_address.district,
- "detailAddress": default_address.detail_address
- }
-
- return success_response({
- "id": user.id,
- "username": user.username,
- "realName": user.real_name,
- "phone": user.phone,
- "role": user.role,
- "regionId": user.region_id,
- "regionName": region_name,
- "address": address_info
- })
- @router.put("/profile", response_model=dict)
- def update_profile(
- request: dict,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """更新供应商个人资料"""
- user = user_crud.get(db, current_user.get("user_id"))
-
- if not user:
- return error_response(404, "用户不存在")
-
- payload = _resolve_request_payload(request, current_user)
- updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
- user = user_crud.update_with_history(db, current_user.get("user_id"), payload, updater)
-
- return success_response({
- "id": user.id,
- "realName": user.real_name,
- "phone": user.phone
- })
- # ============ 询价单管理 ============
- @router.get("/inquiries", response_model=dict)
- def list_inquiries(
- page: int = Query(1, ge=1, description="页码"),
- pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取供应商可响应的询价单列表"""
- from app.crud.inquiry import inquiry_crud, inquiry_item_crud
- from app.crud.quotation import quotation_crud
- user = user_crud.get(db, current_user.get("user_id"))
- if not user or not user.region_id:
- return error_response(400, "用户未绑定区域")
- inquiries, total = inquiry_crud.get_multi_with_filters(
- db,
- region_id=user.region_id,
- status="published",
- skip=(page - 1) * pageSize,
- limit=pageSize
- )
- inquiry_list = []
- for inquiry in inquiries:
- has_quoted = quotation_crud.get_latest_by_inquiry_and_supplier(db, inquiry.id, user.id) is not None
- inquiry_list.append({
- "id": inquiry.id,
- "title": inquiry.title,
- "regionId": inquiry.region_id,
- "status": inquiry.status,
- "publishTime": str(inquiry.publish_time) if inquiry.publish_time else None,
- "closeTime": str(inquiry.close_time) if inquiry.close_time else None,
- "remark": inquiry.remark,
- "createAt": str(inquiry.create_at) if inquiry.create_at else None,
- "hasQuoted": has_quoted
- })
- return success_response({
- "list": inquiry_list,
- "total": total,
- "page": page,
- "pageSize": pageSize,
- "totalPages": (total + pageSize - 1) // pageSize
- })
- @router.get("/inquiries/{inquiry_id}", response_model=dict)
- def get_inquiry_detail(
- inquiry_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取询价单详情"""
- from app.crud.inquiry import inquiry_crud, inquiry_item_crud
- from app.crud.product import product_crud
- from app.crud.category import category_crud
- from app.crud.unit import unit_crud
- from app.crud.quotation import quotation_crud
- user = user_crud.get(db, current_user.get("user_id"))
- if not user or not user.region_id:
- return error_response(400, "用户未绑定区域")
- inquiry = inquiry_crud.get(db, inquiry_id)
- if not inquiry:
- return error_response(404, "询价单不存在")
- if inquiry.region_id != user.region_id:
- return error_response(403, "无权查看此询价单")
- items = inquiry_item_crud.get_by_inquiry(db, inquiry_id)
- quotation = quotation_crud.get_latest_by_inquiry_and_supplier(db, inquiry_id, user.id)
- latest_quote_price_map = {}
- if quotation:
- from app.crud.quotation import quotation_item_crud
- quotation_items = quotation_item_crud.get_by_quotation(db, quotation.id)
- for quote_item in quotation_items:
- latest_quote_price_map[quote_item.product_id] = float(quote_item.price) if quote_item.price else 0
- item_details = []
- for item in items:
- product = product_crud.get(db, item.product_id)
- category_name = None
- first_category_id = None
- first_category_name = None
- first_category_image = None
- first_category_sort_order = 0
- second_category_id = None
- second_category_name = None
- second_category_image = None
- second_category_sort_order = 0
- product_sort_order = int(getattr(product, "sort_order", None) or 0) if product else 0
- unit_name = None
- if product and product.category_id:
- category = category_crud.get(db, product.category_id)
- category_name = category.name if category else None
- if category:
- if category.parent_id:
- parent_category = category_crud.get(db, category.parent_id)
- first_category_id = parent_category.id if parent_category else category.id
- first_category_name = parent_category.name if parent_category else category.name
- first_category_image = parent_category.image_url if parent_category else category.image_url
- first_category_sort_order = int(parent_category.sort_order or 0) if parent_category else int(category.sort_order or 0)
- second_category_id = category.id
- second_category_name = category.name
- second_category_image = category.image_url
- second_category_sort_order = int(category.sort_order or 0)
- else:
- first_category_id = category.id
- first_category_name = category.name
- first_category_image = category.image_url
- first_category_sort_order = int(category.sort_order or 0)
- second_category_sort_order = int(category.sort_order or 0)
- if product and product.unit_id:
- unit = unit_crud.get(db, product.unit_id)
- unit_name = unit.name if unit else None
- item_details.append({
- "id": item.id,
- "productId": item.product_id,
- "productName": product.name if product else None,
- "description": product.description if product else None,
- "imageUrl": product.image_url if product else None,
- "categoryId": product.category_id if product else None,
- "categoryName": category_name,
- "firstCategoryId": first_category_id,
- "firstCategoryName": first_category_name,
- "firstCategoryImage": first_category_image,
- "secondCategoryId": second_category_id,
- "secondCategoryName": second_category_name,
- "secondCategoryImage": second_category_image,
- "unitId": product.unit_id if product else None,
- "unitName": unit_name,
- "measureValue": float(product.measure_value) if product and product.measure_value is not None else None,
- "lastPrice": latest_quote_price_map.get(item.product_id),
- "createAt": str(item.create_at) if item.create_at else None,
- "_firstCategorySortOrder": first_category_sort_order,
- "_secondCategorySortOrder": second_category_sort_order,
- "_productSortOrder": product_sort_order
- })
- # 与客户端首页商品区一致:一级分类排序 -> 二级分类排序 -> 商品排序 -> 名称 -> id
- item_details.sort(
- key=lambda row: (
- int(row.get("_firstCategorySortOrder") or 0),
- int(row.get("_secondCategorySortOrder") or 0),
- int(row.get("_productSortOrder") or 0),
- (row.get("productName") or "").lower(),
- row.get("productId") or "",
- )
- )
- for row in item_details:
- row.pop("_firstCategorySortOrder", None)
- row.pop("_secondCategorySortOrder", None)
- row.pop("_productSortOrder", None)
- return success_response({
- "id": inquiry.id,
- "title": inquiry.title,
- "regionId": inquiry.region_id,
- "status": inquiry.status,
- "publishTime": str(inquiry.publish_time) if inquiry.publish_time else None,
- "closeTime": str(inquiry.close_time) if inquiry.close_time else None,
- "remark": inquiry.remark,
- "createAt": str(inquiry.create_at) if inquiry.create_at else None,
- "items": item_details,
- "hasQuoted": quotation is not None,
- "quotationId": quotation.id if quotation else None,
- "quotationStatus": quotation.status if quotation else None
- })
- # ============ 报价单管理 ============
- @router.post("/quotations", response_model=dict)
- def submit_quotation(
- request: dict,
- raw_request: Request,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """提交报价单"""
- from app.crud.inquiry import inquiry_crud, inquiry_item_crud
- from app.crud.quotation import quotation_crud, quotation_item_crud
- from app.crud.product import product_crud
- from decimal import Decimal
- user = user_crud.get(db, current_user.get("user_id"))
- if not user or not user.region_id:
- return error_response(400, "用户未绑定区域")
- req_payload = request if isinstance(request, dict) else {}
- # 兜底:若中间件未生效导致路由仍收到密文信封,则在路由内自行解密
- if int(req_payload.get("enc", 0) or 0) == 1 and isinstance(req_payload.get("edata"), str):
- try:
- user_id_for_crypto = str(current_user.get("user_id") or "")
- ts = int(req_payload.get("ts"))
- nonce = str(req_payload.get("nonce") or "")
- if not user_id_for_crypto or not nonce:
- return error_response(400, "加密报文缺少必要字段")
- if not settings.API_ENCRYPT_FIXED_SECRET:
- return error_response(500, "服务端加密配置缺失")
- key = api_crypto.derive_key(
- fixed_secret=settings.API_ENCRYPT_FIXED_SECRET,
- user_id=user_id_for_crypto,
- ts=ts,
- nonce=nonce,
- )
- plain = api_crypto.decrypt_json(str(req_payload.get("edata")), key)
- req_payload = plain.get("data", plain) if isinstance(plain, dict) else {}
- if not isinstance(req_payload, dict):
- req_payload = {}
- except Exception as exc:
- logger.warning("submit_quotation decrypt fallback failed: err=%s", exc)
- return error_response(400, "密文解密失败")
- def _extract_payload(d: dict) -> dict:
- """兼容不同层级的请求体结构:优先返回包含 inquiryId/items 的那层 dict。"""
- if not isinstance(d, dict):
- return {}
- if "inquiryId" in d or "items" in d:
- return d
- for k in ("data", "payload", "body"):
- v = d.get(k)
- if isinstance(v, dict):
- found = _extract_payload(v)
- if found:
- return found
- return d
- payload = _extract_payload(req_payload)
- inquiry_id = payload.get("inquiryId")
- items = payload.get("items", [])
- # 诊断:强制打印到 stdout(会落到 logs/app.log),仅输出 keys/长度/数量
- try:
- keys = list(payload.keys()) if isinstance(payload, dict) else []
- iid_len = len(str(inquiry_id)) if inquiry_id is not None else None
- items_len = len(items) if isinstance(items, list) else None
- print(
- f"[submit_quotation debug] keys={keys} inquiryId={repr(inquiry_id)} inquiryId_len={iid_len} items_len={items_len}",
- flush=True,
- )
- except Exception:
- pass
- if not inquiry_id:
- return error_response(400, "询价单ID不能为空")
- if not items:
- return error_response(400, "报价项不能为空")
- inquiry = inquiry_crud.get(db, inquiry_id)
- if not inquiry:
- return error_response(404, "询价单不存在")
- if inquiry.region_id != user.region_id:
- return error_response(403, "无权对此询价单报价")
- if inquiry.status != "published":
- return error_response(400, "询价单已关闭")
- # 支持同一询价单多次报价:每次提交都创建一张新报价单
- inquiry_items = inquiry_item_crud.get_by_inquiry(db, inquiry_id)
- inquiry_product_ids = set(item.product_id for item in inquiry_items)
- submitted_product_ids = set(item.get("productId") for item in items)
- if submitted_product_ids != inquiry_product_ids:
- return error_response(400, "报价商品必须与询价商品一致")
- creator = {"user_id": user.id, "real_name": user.real_name}
- quotation_data = {
- "inquiry_id": inquiry_id,
- "supplier_id": user.id,
- "region_id": user.region_id,
- "status": "submitted",
- "submit_time": datetime.now()
- }
- quotation_items_data = []
- total_amount = Decimal("0")
- for item in items:
- product_id = item.get("productId")
- unit_id = item.get("unitId")
- if not unit_id:
- product = product_crud.get(db, product_id) if product_id else None
- unit_id = product.unit_id if product else None
- if not unit_id:
- return error_response(400, f"商品 {product_id} 缺少单位信息,请传 unitId")
- price = Decimal(str(item.get("price", 0)))
- total_amount += price
- quotation_items_data.append({
- "product_id": product_id,
- "unit_id": unit_id,
- "price": price
- })
- quotation_data["total_amount"] = total_amount
- quotation = quotation_crud.create(db, quotation_data, creator)
- quotation_item_crud.create_items(db, quotation.id, quotation_items_data, creator)
- return success_response({
- "id": quotation.id,
- "totalAmount": float(quotation.total_amount),
- "status": quotation.status,
- "submitTime": str(quotation.submit_time) if quotation.submit_time else None
- })
- @router.get("/quotations", response_model=dict)
- def list_my_quotations(
- page: int = Query(1, ge=1, description="页码"),
- pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
- inquiryId: Optional[str] = Query(None, description="询价单ID"),
- status: Optional[str] = Query(None, description="报价状态,如 selected=已采纳"),
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取我的报价列表"""
- from app.crud.inquiry import inquiry_crud
- from app.crud.quotation import quotation_crud
- user = user_crud.get(db, current_user.get("user_id"))
- if not user:
- return error_response(404, "用户不存在")
- quotations, total = quotation_crud.get_multi_with_filters(
- db,
- supplier_id=user.id,
- inquiry_id=inquiryId,
- status=status,
- skip=(page - 1) * pageSize,
- limit=pageSize
- )
- inquiry_map = {}
- quotation_list = []
- for q in quotations:
- if q.inquiry_id and q.inquiry_id not in inquiry_map:
- inquiry = inquiry_crud.get(db, q.inquiry_id)
- inquiry_map[q.inquiry_id] = inquiry.title if inquiry else None
- quotation_list.append({
- "id": q.id,
- "inquiryId": q.inquiry_id,
- "inquiryTitle": inquiry_map.get(q.inquiry_id),
- "status": q.status,
- "totalAmount": float(q.total_amount) if q.total_amount else 0,
- "submitTime": str(q.submit_time) if q.submit_time else None,
- })
- return success_response({
- "list": quotation_list,
- "total": total,
- "page": page,
- "pageSize": pageSize,
- "totalPages": (total + pageSize - 1) // pageSize
- })
- @router.get("/quotations/{quotation_id}", response_model=dict)
- def get_my_quotation(
- quotation_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取我的报价单详情"""
- from app.crud.inquiry import inquiry_crud, inquiry_item_crud
- from app.crud.quotation import quotation_crud, quotation_item_crud
- from app.crud.product import product_crud
- user = user_crud.get(db, current_user.get("user_id"))
- if not user:
- return error_response(404, "用户不存在")
- quotation = quotation_crud.get(db, quotation_id)
- if not quotation:
- return error_response(404, "报价单不存在")
- if quotation.supplier_id != user.id:
- return error_response(403, "无权查看此报价单")
- items = quotation_item_crud.get_by_quotation(db, quotation_id)
- inquiry_title = None
- if quotation.inquiry_id:
- inquiry = inquiry_crud.get(db, quotation.inquiry_id)
- inquiry_title = inquiry.title if inquiry else None
- item_details = []
- for item in items:
- product = product_crud.get(db, item.product_id)
- item_details.append({
- "id": item.id,
- "productId": item.product_id,
- "productName": product.name if product else None,
- "unitId": item.unit_id,
- "price": float(item.price) if item.price else 0,
- "createAt": str(item.create_at) if item.create_at else None
- })
- return success_response({
- "id": quotation.id,
- "inquiryId": quotation.inquiry_id,
- "inquiryTitle": inquiry_title,
- "status": quotation.status,
- "totalAmount": float(quotation.total_amount) if quotation.total_amount else 0,
- "submitTime": str(quotation.submit_time) if quotation.submit_time else None,
- "items": item_details
- })
- # ============ 价格清单 - 供应商查看 ============
- @router.get("/price-lists", response_model=dict)
- def list_my_price_lists(
- page: int = Query(1, ge=1, description="页码"),
- pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
- status: Optional[str] = Query(None, description="状态"),
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取供应商的价格清单列表"""
- from app.crud.price_list import price_list_crud
- from app.crud.region import region_crud
- user = user_crud.get(db, current_user.get("user_id"))
- if not user:
- return error_response(404, "用户不存在")
- price_lists, total = price_list_crud.get_multi_with_filters(
- db,
- supplier_id=user.id,
- status=status,
- skip=(page - 1) * pageSize,
- limit=pageSize
- )
- region_map = {}
- for pl in price_lists:
- if pl.region_id and pl.region_id not in region_map:
- region = region_crud.get(db, pl.region_id)
- region_map[pl.region_id] = region.name if region else None
- return success_response({
- "list": [{
- "id": pl.id,
- "regionId": pl.region_id,
- "regionName": region_map.get(pl.region_id),
- "version": pl.version,
- "status": pl.status,
- "effectiveTime": str(pl.effective_time) if pl.effective_time else None,
- "publishedTime": str(pl.published_time) if pl.published_time else None,
- "remark": pl.remark,
- "createAt": str(pl.create_at) if pl.create_at else None
- } for pl in price_lists],
- "total": total,
- "page": page,
- "pageSize": pageSize,
- "totalPages": (total + pageSize - 1) // pageSize
- })
- @router.get("/price-lists/{price_list_id}", response_model=dict)
- def get_my_price_list_detail(
- price_list_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取供应商价格清单详情"""
- from app.crud.price_list import price_list_crud, price_list_item_crud
- from app.crud.region import region_crud
- from app.crud.product import product_crud
- from app.crud.unit import unit_crud
- user = user_crud.get(db, current_user.get("user_id"))
- if not user:
- return error_response(404, "用户不存在")
- price_list = price_list_crud.get(db, price_list_id)
- if not price_list:
- return error_response(404, "价格清单不存在")
- if price_list.supplier_id != user.id:
- return error_response(403, "无权查看此价格清单")
- items = price_list_item_crud.get_by_price_list(db, price_list_id)
- # 获取关联信息
- region = region_crud.get(db, price_list.region_id)
- region_name = region.name if region else None
- item_details = []
- for item in items:
- product = product_crud.get(db, item.product_id)
- category_name = None
- if product and product.category_id:
- category = category_crud.get(db, product.category_id)
- category_name = category.name if category else None
- unit_name = None
- if item.unit_id:
- u = unit_crud.get(db, item.unit_id)
- unit_name = u.name if u else None
- item_details.append({
- "productId": item.product_id,
- "productName": product.name if product else None,
- "categoryName": category_name,
- "unitId": item.unit_id,
- "unitName": unit_name,
- "price": float(item.price) if item.price else 0,
- "imageUrl": product.image_url if product else None
- })
- return success_response({
- "id": price_list.id,
- "regionId": price_list.region_id,
- "regionName": region_name,
- "version": price_list.version,
- "status": price_list.status,
- "effectiveTime": str(price_list.effective_time) if price_list.effective_time else None,
- "publishedTime": str(price_list.published_time) if price_list.published_time else None,
- "remark": price_list.remark,
- "createAt": str(price_list.create_at) if price_list.create_at else None,
- "items": item_details
- })
- # ============ 配送任务管理 ============
- @router.get("/tasks", response_model=dict)
- def list_supplier_tasks(
- page: int = Query(1, ge=1, description="页码"),
- pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
- supplierPrepareStatus: Optional[str] = Query(
- None,
- description="供应商备货状态 supplier_prepare_status;逗号多选,如 stocking,prepared",
- ),
- status: Optional[str] = Query(
- None,
- description="兼容:任务主状态 status;未传 supplierPrepareStatus 时生效",
- ),
- keyword: Optional[str] = Query(None, description="检索:配送单号、订单号、联系人、电话"),
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取供应商的配送任务列表(含商品缩略列表;供货合计按价格清单还原供货单价后汇总)"""
- from sqlalchemy import and_
- from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
- from app.crud.order import order_crud, order_item_crud
- from app.crud.product import product_crud
- from app.crud.unit import unit_crud
- from app.models.models import BizPriceListItem
- user = user_crud.get(db, current_user.get("user_id"))
- if not user:
- return error_response(404, "用户不存在")
- tasks, total = delivery_task_crud.get_multi_with_filters(
- db,
- supplier_id=user.id,
- supplier_prepare_status=supplierPrepareStatus,
- status=None if supplierPrepareStatus else status,
- keyword=keyword,
- skip=(page - 1) * pageSize,
- limit=pageSize
- )
- result = []
- for task in tasks:
- region_name = None
- if task.region_id:
- region = region_crud.get(db, task.region_id)
- region_name = region.name if region else None
- # 任务行 + 供货小计(与任务详情中价格清单逻辑一致)
- task_items = delivery_task_item_crud.get_by_task(db, task.id)
- total_items = len(task_items)
- delivered_items = len([i for i in task_items if i.status == "delivered"])
- order = order_crud.get(db, task.order_id) if task.order_id else None
- line_items = []
- total_quantity = 0.0
- supplier_quote_total = 0.0
- for ti in task_items:
- product = product_crud.get(db, ti.product_id)
- unit_name = None
- supplier_quote_line = None
- oi = None
- if ti.order_item_id:
- oi = order_item_crud.get(db, ti.order_item_id)
- if oi and oi.unit_id:
- u = unit_crud.get(db, oi.unit_id)
- unit_name = u.name if u else None
- if order and oi:
- pli = db.query(BizPriceListItem).filter(
- and_(
- BizPriceListItem.price_list_id == order.price_list_id,
- BizPriceListItem.product_id == oi.product_id,
- BizPriceListItem.unit_id == oi.unit_id,
- BizPriceListItem.is_del == 0,
- )
- ).first()
- if pli is not None and pli.price is not None:
- unit_price = _supplier_quote_unit_yuan_from_pli(pli)
- if unit_price is not None:
- qty = float(ti.quantity) if ti.quantity else 0.0
- supplier_quote_line = round(qty * unit_price, 2)
- supplier_quote_total += supplier_quote_line
- qty = float(ti.quantity) if ti.quantity else 0.0
- total_quantity += qty
- line_items.append({
- "id": ti.id,
- "productId": ti.product_id,
- "productName": product.name if product else None,
- "quantity": qty,
- "unitName": unit_name,
- "imageUrl": product.image_url if product else None,
- })
- # 获取配送员信息(列表不返回客户信息,仅展示配送员对接用)
- deliver_name = None
- deliver_phone = None
- if task.deliver_id:
- deliver = user_crud.get(db, task.deliver_id)
- if deliver:
- deliver_name = deliver.real_name
- deliver_phone = deliver.phone
- result.append({
- "id": task.id,
- "taskNo": task.task_no,
- "orderId": task.order_id,
- "regionId": task.region_id,
- "regionName": region_name,
- "supplierId": task.supplier_id,
- "deliverId": task.deliver_id,
- "deliverName": deliver_name,
- "deliverPhone": deliver_phone,
- "status": task.status,
- "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
- "effectiveTime": str(task.effective_time) if task.effective_time else None,
- "pickupTime": str(task.pickup_time) if task.pickup_time else None,
- "pickupRemark": task.pickup_remark,
- "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
- "totalItems": total_items,
- "deliveredItems": delivered_items,
- "totalQuantity": round(total_quantity, 3),
- "supplierQuoteTotal": round(supplier_quote_total, 2),
- "items": line_items,
- "createAt": str(task.create_at) if task.create_at else None,
- "supplierSettlementStatus": (
- (getattr(order, "supplier_settlement_status", None) or "pending")
- if order
- else "pending"
- ),
- })
- return success_response({
- "list": result,
- "total": total,
- "page": page,
- "pageSize": pageSize,
- "totalPages": (total + pageSize - 1) // pageSize
- })
- @router.get("/tasks/{task_id}", response_model=dict)
- def get_supplier_task_detail(
- task_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """获取供应商配送任务详情(订单仅返回客户订单号;商品行含主图与规格;单价由价格清单行 price(客户最终单价)与 pricing_coefficient 反推供货价)"""
- from sqlalchemy import and_
- from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
- from app.crud.order import order_crud, order_item_crud
- from app.crud.product import product_crud
- from app.crud.unit import unit_crud
- from app.models.models import BizPriceListItem
- user = user_crud.get(db, current_user.get("user_id"))
- if not user:
- return error_response(404, "用户不存在")
- task = delivery_task_crud.get(db, task_id)
- if not task:
- return error_response(404, "任务不存在")
- if task.supplier_id != user.id:
- return error_response(403, "无权查看此任务")
- order = None
- order_info = None
- if task.order_id:
- order = order_crud.get(db, task.order_id)
- if order:
- # 供应商备货详情仅展示客户订单号,不下发客户联系人、地址等
- order_info = {
- "orderNo": order.order_no,
- "supplierSettlementStatus": getattr(order, "supplier_settlement_status", None) or "pending",
- "supplierSettlementAt": str(order.supplier_settlement_at)
- if getattr(order, "supplier_settlement_at", None)
- else None,
- }
- deliver_info = None
- if task.deliver_id:
- deliver = user_crud.get(db, task.deliver_id)
- if deliver:
- deliver_info = {
- "deliverId": deliver.id,
- "deliverName": deliver.real_name,
- "phone": deliver.phone
- }
- task_items = delivery_task_item_crud.get_by_task(db, task_id)
- item_details = []
- for item in task_items:
- product = product_crud.get(db, item.product_id)
- unit_name = None
- supplier_quote_unit = None
- supplier_quote_line = None
- product_image_url = None
- product_measure_value = None
- product_unit_name = None
- if product:
- product_image_url = product.image_url
- if product.measure_value is not None:
- product_measure_value = float(product.measure_value)
- if product.unit_id:
- pu = unit_crud.get(db, product.unit_id)
- product_unit_name = pu.name if pu else None
- oi = None
- if item.order_item_id:
- oi = order_item_crud.get(db, item.order_item_id)
- if oi and oi.unit_id:
- u = unit_crud.get(db, oi.unit_id)
- unit_name = u.name if u else None
- if order and oi:
- pli = db.query(BizPriceListItem).filter(
- and_(
- BizPriceListItem.price_list_id == order.price_list_id,
- BizPriceListItem.product_id == oi.product_id,
- BizPriceListItem.unit_id == oi.unit_id,
- BizPriceListItem.is_del == 0,
- )
- ).first()
- if pli is not None and pli.price is not None:
- supplier_quote_unit = _supplier_quote_unit_yuan_from_pli(pli)
- if supplier_quote_unit is not None:
- qty = float(item.quantity) if item.quantity else 0.0
- supplier_quote_line = round(qty * supplier_quote_unit, 2)
- item_details.append({
- "id": item.id,
- "orderItemId": item.order_item_id,
- "productId": item.product_id,
- "productName": product.name if product else None,
- "imageUrl": product_image_url,
- "measureValue": product_measure_value,
- "productUnitName": product_unit_name,
- "unitName": unit_name,
- "quantity": float(item.quantity) if item.quantity else 0,
- "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0,
- "supplierQuoteUnit": supplier_quote_unit,
- "supplierQuoteLine": supplier_quote_line,
- "status": item.status,
- "deliveryTime": str(item.delivery_time) if item.delivery_time else None
- })
- prepared_images = [
- a.file_url
- for a in attachment_crud.get_by_business(db, "delivery_prepare", task_id)
- if getattr(a, "file_url", None)
- ]
- if (not prepared_images) and getattr(task, "pickup_image_url", None):
- prepared_images = [s.strip() for s in str(task.pickup_image_url).split(",") if s and str(s).strip()]
- return success_response({
- "id": task.id,
- "taskNo": task.task_no,
- "orderId": task.order_id,
- "regionId": task.region_id,
- "status": task.status,
- "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
- "effectiveTime": str(task.effective_time) if task.effective_time else None,
- "pickupTime": str(task.pickup_time) if task.pickup_time else None,
- "pickupRemark": task.pickup_remark,
- "pickupImageUrl": task.pickup_image_url,
- "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
- "deliveryRemark": task.delivery_remark,
- "deliveryImageUrl": task.delivery_image_url,
- "preparedImages": prepared_images,
- "order": order_info,
- "deliver": deliver_info,
- "items": item_details,
- "createAt": str(task.create_at) if task.create_at else None
- })
- def _notify_deliver_stock_ready(db, task, user, remark: str = None) -> bool:
- """备货完成后通知配送员(微信订阅消息,失败不阻断主流程)"""
- if not task.deliver_id:
- return False
- try:
- from app.core.wechat_message import WechatMessageService
- deliver = user_crud.get(db, task.deliver_id)
- if deliver and hasattr(deliver, "openid"):
- WechatMessageService.send_delivery_notice(
- openid=deliver.openid,
- task_no=task.task_no,
- supplier_name=user.real_name or "供应商",
- address="商品已备好,请尽快取货",
- remark=remark or "供应商已备货完成,请及时取货",
- )
- return True
- except Exception:
- pass
- return False
- @router.post("/tasks/{task_id}/start-stock", response_model=dict)
- def supplier_start_stock(
- task_id: str,
- request: dict,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier),
- ):
- """供应商开始备货:assigned -> supplier_stocking,并写入订单状态日志(配送进度)"""
- from app.crud.delivery import delivery_task_crud
- user = user_crud.get(db, current_user.get("user_id"))
- if not user:
- return error_response(404, "用户不存在")
- task = delivery_task_crud.get(db, task_id)
- if not task:
- return error_response(404, "任务不存在")
- if task.supplier_id != user.id:
- return error_response(403, "无权操作此任务")
- if (getattr(task, "supplier_prepare_status", None) or "pending_prepare") != "pending_prepare":
- return error_response(400, "当前状态不可开始备货(需为待备货/已分配)")
- payload = _resolve_request_payload(request, current_user)
- remark = (payload.get("remark") or "").strip() or None
- task = delivery_task_crud.update_status(
- db,
- task_id=task_id,
- new_status="supplier_stocking",
- operator_id=user.id,
- remark=remark,
- operator_real_name=user.real_name,
- operator_role="supplier",
- )
- if not task:
- return error_response(400, "操作失败")
- return success_response({
- "id": task.id,
- "taskNo": task.task_no,
- "status": task.status,
- "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
- "delivererStatus": getattr(task, "deliverer_status", None),
- "message": "已开始备货",
- })
- @router.post("/tasks/{task_id}/finish-stock", response_model=dict)
- def supplier_finish_stock(
- task_id: str,
- request: dict,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier),
- ):
- """供应商备货完成:supplier_stocking -> supplier_ready(待配送员取货),写入订单日志并可通知配送员"""
- from app.crud.delivery import delivery_task_crud
- user = user_crud.get(db, current_user.get("user_id"))
- if not user:
- return error_response(404, "用户不存在")
- task = delivery_task_crud.get(db, task_id)
- if not task:
- return error_response(404, "任务不存在")
- if task.supplier_id != user.id:
- return error_response(403, "无权操作此任务")
- ss = getattr(task, "supplier_prepare_status", None) or "pending_prepare"
- if ss == "pending_prepare":
- return error_response(400, "请先开始备货后再确认备货完成")
- if ss not in ("stocking", "prepared"):
- return error_response(400, "当前状态不可确认备货完成")
- if ss == "prepared":
- ds = (getattr(task, "deliverer_status", None) or "").strip().lower()
- if ds != "pending_pickup":
- return error_response(400, "配送员已取货,备货凭证不可再修改")
- payload = _resolve_request_payload(request, current_user)
- remark = (payload.get("remark") or payload.get("preparedRemark") or "").strip() or None
- image_urls = _normalize_image_urls(payload.get("imageUrls"))
- if not image_urls:
- prep_img = payload.get("preparedImageUrl") or payload.get("pickupImageUrl")
- image_urls = _normalize_image_urls(prep_img)
- if not image_urls:
- return error_response(400, "请上传备货完成凭证照片")
- if ss == "stocking":
- task = delivery_task_crud.update_status(
- db,
- task_id=task_id,
- new_status="supplier_ready",
- operator_id=user.id,
- remark=remark,
- image_urls=image_urls,
- operator_real_name=user.real_name,
- operator_role="supplier",
- )
- if not task:
- return error_response(400, "操作失败")
- else:
- task = delivery_task_crud.get(db, task_id)
- if not task:
- return error_response(404, "任务不存在")
- _replace_delivery_prepare_attachments(
- db,
- task_id=task_id,
- image_urls=image_urls,
- uploader_id=user.id,
- uploader_name=user.real_name,
- )
- db.commit()
- deliver_notified = _notify_deliver_stock_ready(db, task, user, remark) if ss == "stocking" else False
- return success_response({
- "id": task.id,
- "taskNo": task.task_no,
- "status": task.status,
- "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None),
- "delivererStatus": getattr(task, "deliverer_status", None),
- "message": ("已确认备货完成" if ss == "stocking" else "备货凭证已更新")
- + (",已通知配送员" if deliver_notified else ""),
- })
- # 兼容旧路径:与 start-stock / finish-stock 行为一致
- @router.post("/tasks/{task_id}/confirm-pickup", response_model=dict)
- def supplier_confirm_pickup_deprecated(
- task_id: str,
- request: dict,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier),
- ):
- """[兼容] 请改用 POST /tasks/{id}/start-stock"""
- return supplier_start_stock(task_id, request, db, current_user)
- @router.post("/tasks/{task_id}/confirm-prepared", response_model=dict)
- def supplier_confirm_prepared_deprecated(
- task_id: str,
- request: dict,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier),
- ):
- """[兼容] 请改用 POST /tasks/{id}/finish-stock"""
- return supplier_finish_stock(task_id, request, db, current_user)
- @router.get("/dashboard/statistics", response_model=dict)
- def supplier_dashboard_statistics(
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """供应商首页统计(询价/报价/任务概览)"""
- from app.crud.delivery import delivery_task_crud
- from app.models.models import BizInquiry, BizQuotation
- from sqlalchemy import and_
- uid = current_user.get("user_id")
- user = user_crud.get(db, uid)
- rid = user.region_id if user else None
- q_inq = db.query(BizInquiry).filter(
- and_(BizInquiry.is_del == 0, BizInquiry.status == "published")
- )
- if rid:
- q_inq = q_inq.filter(BizInquiry.region_id == rid)
- pending_inq = q_inq.count()
- my_quo = db.query(BizQuotation).filter(
- and_(BizQuotation.is_del == 0, BizQuotation.supplier_id == uid)
- ).count()
- pending_tasks, _ = delivery_task_crud.get_by_supplier(
- db, supplier_id=uid, status="assigned", skip=0, limit=500
- )
- stocking, _ = delivery_task_crud.get_by_supplier(
- db, supplier_id=uid, status="supplier_stocking", skip=0, limit=500
- )
- return success_response({
- "pendingInquiries": pending_inq,
- "myQuotations": my_quo,
- "pendingTasks": len(pending_tasks) + len(stocking),
- "todayDeliveries": 0,
- })
- @router.get("/products/{product_id}", response_model=dict)
- def get_supplier_product_detail(
- product_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(require_supplier)
- ):
- """供应商端商品详情:轮播、基础信息、参数、富文本、详情媒体"""
- from app.crud.product import product_crud
- from app.crud.unit import unit_crud
- from app.crud.product_banner import product_banner
- from app.crud.product_parameter_value import product_parameter_value
- from app.crud.product_detail_media import product_detail_media
- product = product_crud.get(db, product_id)
- if not product or getattr(product, "is_del", 0) == 1:
- return error_response(404, "商品不存在")
- unit = unit_crud.get(db, product.unit_id) if product.unit_id else None
- banners_raw = product_banner.get_by_product(db, product_id)
- banners = [{
- "mediaType": b.media_type,
- "fileUrl": b.file_url,
- "thumbnailUrl": b.thumbnail_url,
- "sortOrder": b.sort_order,
- } for b in banners_raw]
- pv_rows = product_parameter_value.get_by_product_with_detail(db, product_id)
- parameter_values = []
- for row in pv_rows:
- val = row.get("param_value")
- if val is None or str(val).strip() == "":
- continue
- parameter_values.append({
- "parameterId": row.get("parameter_id"),
- "parameterName": row.get("parameter_name") or "",
- "paramValue": str(val),
- })
- media_raw = product_detail_media.get_by_product(db, product_id)
- detail_media = []
- for m in media_raw:
- if m.content_type not in ("image", "video"):
- continue
- if not m.file_url:
- continue
- detail_media.append({
- "id": m.id,
- "contentType": m.content_type,
- "fileUrl": m.file_url,
- "thumbnailUrl": m.thumbnail_url,
- "description": m.description,
- "sortOrder": m.sort_order,
- })
- return success_response({
- "product": {
- "id": product.id,
- "name": product.name,
- "unitId": product.unit_id,
- "unitName": unit.name if unit else None,
- "measureValue": float(product.measure_value) if product.measure_value is not None else None,
- "detailContent": product.detail_content or "",
- },
- "banners": banners,
- "parameterValues": parameter_values,
- "detailMedia": detail_media,
- })
|