| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313 |
- """小程序端供应商接口"""
- from decimal import Decimal
- from typing import Optional
- from datetime import datetime
- 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.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=["小程序端-供应商接口"])
- 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 _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)
- ):
- """创建地址"""
- request["user_id"] = current_user.get("user_id")
- creator = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")}
-
- # 如果设置为默认,先取消其他默认
- if request.get("isDefault"):
- address_crud.set_default(db, current_user.get("user_id"), "")
-
- address = address_crud.create(db, request, 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, "地址不存在")
-
- # 如果设置为默认,先取消其他默认
- if request.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, request, 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, "用户不存在")
-
- 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"), request, 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
- second_category_id = None
- second_category_name = None
- 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
- second_category_id = category.id
- second_category_name = category.name
- else:
- first_category_id = category.id
- first_category_name = category.name
- 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,
- "secondCategoryId": second_category_id,
- "secondCategoryName": second_category_name,
- "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
- })
- 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,
- 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 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, "用户未绑定区域")
- inquiry_id = request.get("inquiryId")
- items = request.get("items", [])
- 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:
- price = Decimal(str(item.get("price", 0)))
- total_amount += price
- quotation_items_data.append({
- "product_id": item.get("productId"),
- "unit_id": item.get("unitId"),
- "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
- })
- 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,
- }
- 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, "当前状态不可开始备货(需为待备货/已分配)")
- remark = (request.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, "配送员已取货,备货凭证不可再修改")
- remark = (request.get("remark") or request.get("preparedRemark") or "").strip() or None
- image_urls = _normalize_image_urls(request.get("imageUrls"))
- if not image_urls:
- prep_img = request.get("preparedImageUrl") or request.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,
- })
|