| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506 |
- """小程序端供应商接口"""
- 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.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
- from app.utils.batch_lookup import prefetch_products_categories_units
- from app.models.models import BasProductCategory
- product_ids = [str(item.product_id) for item in items if item.product_id]
- product_map, _cat_name_map, unit_map = prefetch_products_categories_units(db, product_ids)
- # 分类需父子信息,主数据一次载入
- all_categories = db.query(BasProductCategory).filter(BasProductCategory.is_del == 0).all()
- cat_by_id = {str(c.id): c for c in all_categories}
- item_details = []
- for item in items:
- product = product_map.get(str(item.product_id or ""))
- 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 = cat_by_id.get(str(product.category_id))
- category_name = category.name if category else None
- if category:
- if category.parent_id:
- parent_category = cat_by_id.get(str(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_name = unit_map.get(str(product.unit_id))
- 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,
- "spec": getattr(product, "spec", None) if product else None,
- "lastPrice": latest_quote_price_map.get(item.product_id),
- "suggestedSupplierPrice": (
- float(product.suggested_supplier_price)
- if product and product.suggested_supplier_price is not None
- else None
- ),
- "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
- from app.utils.batch_lookup import prefetch_products_categories_units
- product_ids = [str(item.product_id) for item in items if item.product_id]
- product_map, _cat_map, _unit_map = prefetch_products_categories_units(db, product_ids)
- item_details = []
- for item in items:
- product = product_map.get(str(item.product_id or ""))
- 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.utils.batch_lookup import prefetch_products_categories_units
- 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
- product_ids = [str(item.product_id) for item in items if item.product_id]
- extra_unit_ids = [str(item.unit_id) for item in items if item.unit_id]
- product_map, category_map, unit_map = prefetch_products_categories_units(
- db, product_ids, extra_unit_ids
- )
- item_details = []
- for item in items:
- product = product_map.get(str(item.product_id or ""))
- category_id = str(product.category_id) if product and product.category_id else None
- unit_id = str(item.unit_id) if item.unit_id else None
- item_details.append({
- "productId": item.product_id,
- "productName": product.name if product else None,
- "categoryName": category_map.get(category_id) if category_id else None,
- "unitId": item.unit_id,
- "unitName": unit_map.get(unit_id) if unit_id else None,
- "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
- 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
- )
- from app.utils.batch_lookup import prefetch_products_categories_units
- from app.models.models import BizOrderItem, SysRegion, SysUser, BizOrder
- task_ids = [t.id for t in tasks]
- all_task_items = []
- if task_ids:
- from app.models.models import BizDeliveryTaskItem
- all_task_items = db.query(BizDeliveryTaskItem).filter(
- and_(
- BizDeliveryTaskItem.task_id.in_(task_ids),
- BizDeliveryTaskItem.is_del == 0,
- )
- ).all()
- items_by_task: dict[str, list] = {}
- for ti in all_task_items:
- items_by_task.setdefault(str(ti.task_id), []).append(ti)
- order_ids = [str(t.order_id) for t in tasks if t.order_id]
- orders = []
- if order_ids:
- orders = db.query(BizOrder).filter(and_(BizOrder.id.in_(order_ids), BizOrder.is_del == 0)).all()
- order_map = {str(o.id): o for o in orders}
- region_ids = [str(t.region_id) for t in tasks if t.region_id]
- regions = []
- if region_ids:
- regions = db.query(SysRegion).filter(and_(SysRegion.id.in_(region_ids), SysRegion.is_del == 0)).all()
- region_map = {str(r.id): r for r in regions}
- deliver_ids = [str(t.deliver_id) for t in tasks if t.deliver_id]
- delivers = []
- if deliver_ids:
- delivers = db.query(SysUser).filter(and_(SysUser.id.in_(deliver_ids), SysUser.is_del == 0)).all()
- deliver_map = {str(d.id): d for d in delivers}
- product_ids = [str(ti.product_id) for ti in all_task_items if ti.product_id]
- order_item_ids = [str(ti.order_item_id) for ti in all_task_items if ti.order_item_id]
- order_lines = []
- if order_item_ids:
- order_lines = db.query(BizOrderItem).filter(
- and_(BizOrderItem.id.in_(order_item_ids), BizOrderItem.is_del == 0)
- ).all()
- order_line_map = {str(oi.id): oi for oi in order_lines}
- extra_unit_ids = [str(oi.unit_id) for oi in order_lines if oi.unit_id]
- product_map, _cat_map, unit_map = prefetch_products_categories_units(
- db, product_ids, extra_unit_ids
- )
- pl_ids = list({str(o.price_list_id) for o in orders if o.price_list_id})
- pli_map = {}
- if pl_ids:
- plis = db.query(BizPriceListItem).filter(
- and_(BizPriceListItem.price_list_id.in_(pl_ids), BizPriceListItem.is_del == 0)
- ).all()
- for pli in plis:
- pli_map[(str(pli.price_list_id), str(pli.product_id or ""), str(pli.unit_id or ""))] = pli
- result = []
- for task in tasks:
- region = region_map.get(str(task.region_id or ""))
- region_name = region.name if region else None
- task_items = items_by_task.get(str(task.id), [])
- total_items = len(task_items)
- delivered_items = len([i for i in task_items if i.status == "delivered"])
- order = order_map.get(str(task.order_id or "")) if task.order_id else None
- line_items = []
- total_quantity = 0.0
- supplier_quote_total = 0.0
- for ti in task_items:
- product = product_map.get(str(ti.product_id or ""))
- unit_name = None
- supplier_quote_line = None
- oi = order_line_map.get(str(ti.order_item_id or "")) if ti.order_item_id else None
- if oi and oi.unit_id:
- unit_name = unit_map.get(str(oi.unit_id))
- if order and oi:
- pli = pli_map.get((str(order.price_list_id or ""), str(oi.product_id or ""), str(oi.unit_id or "")))
- 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 = deliver_map.get(str(task.deliver_id or "")) if task.deliver_id else None
- deliver_name = deliver.real_name if deliver else None
- deliver_phone = deliver.phone if deliver else None
- 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
- 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)
- from app.utils.batch_lookup import prefetch_products_categories_units
- from app.models.models import BizOrderItem
- product_ids = [str(item.product_id) for item in task_items if item.product_id]
- order_item_ids = [str(item.order_item_id) for item in task_items if item.order_item_id]
- order_lines = []
- if order_item_ids:
- order_lines = db.query(BizOrderItem).filter(
- and_(BizOrderItem.id.in_(order_item_ids), BizOrderItem.is_del == 0)
- ).all()
- order_line_map = {str(oi.id): oi for oi in order_lines}
- extra_unit_ids = []
- for oi in order_lines:
- if oi.unit_id:
- extra_unit_ids.append(str(oi.unit_id))
- product_map, _cat_map, unit_map = prefetch_products_categories_units(
- db, product_ids, extra_unit_ids
- )
- # 批量预取价格清单明细
- pli_map = {}
- if order and order.price_list_id:
- plis = db.query(BizPriceListItem).filter(
- and_(
- BizPriceListItem.price_list_id == order.price_list_id,
- BizPriceListItem.is_del == 0,
- )
- ).all()
- for pli in plis:
- pli_map[(str(pli.product_id or ""), str(pli.unit_id or ""))] = pli
- item_details = []
- for item in task_items:
- product = product_map.get(str(item.product_id or ""))
- unit_name = None
- supplier_quote_unit = None
- supplier_quote_line = None
- product_image_url = None
- product_measure_value = None
- product_spec = None
- product_unit_name = None
- if product:
- product_image_url = product.image_url
- product_spec = getattr(product, "spec", None) or None
- if product.measure_value is not None:
- product_measure_value = float(product.measure_value)
- if product.unit_id:
- product_unit_name = unit_map.get(str(product.unit_id))
- oi = order_line_map.get(str(item.order_item_id or "")) if item.order_item_id else None
- if oi and oi.unit_id:
- unit_name = unit_map.get(str(oi.unit_id))
- if order and oi:
- pli = pli_map.get((str(oi.product_id or ""), str(oi.unit_id or "")))
- 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,
- "spec": product_spec,
- "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,
- "spec": getattr(product, "spec", None) or None,
- "detailContent": product.detail_content or "",
- },
- "banners": banners,
- "parameterValues": parameter_values,
- "detailMedia": detail_media,
- })
|