"""小程序端客户端接口""" import re from typing import Optional from fastapi import APIRouter, Depends, Request, Query from sqlalchemy.orm import Session from sqlalchemy import and_ 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.request_payload import resolve_request_payload from app.models.models import ( BasAddress, BasProduct, BasProductCategory, BasProductUnit, BizDeliveryTask, BizOrderItem, BizPriceListItem, SysUser, ) from app.crud.address import address_crud from app.crud.user import user_crud from app.crud.region import region_crud router = APIRouter(prefix="/api/mini/client", tags=["小程序端-客户端接口"]) def require_client(current_user: dict = Depends(get_current_user)): """验证客户权限""" if current_user.get("role") != "client": return error_response(403, "需要客户权限") return current_user def require_supplier_or_client(current_user: dict = Depends(get_current_user)): """验证供应商或客户权限""" if current_user.get("role") not in ["client", "supplier"]: return error_response(403, "需要供应商或客户权限") return current_user def _resolve_request_payload(request_body: dict, current_user: dict) -> dict: return resolve_request_payload(request_body, current_user, source="mini_client") def _deliverer_contact_phone(du: SysUser) -> Optional[str]: """配送员联系电话:优先 sys_user.phone;为空则尝试 username 为 11 位手机号的情况。""" p = (getattr(du, "phone", None) or "").strip() if p: return p un = (getattr(du, "username", None) or "").strip() if re.fullmatch(r"1[3-9]\d{9}", un): return un return None # ============ 地址管理 ============ def _address_to_client_dict(addr) -> dict: return { "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, } @router.get("/addresses", response_model=dict) def list_addresses( db: Session = Depends(get_db), current_user: dict = Depends(require_client), page: Optional[int] = Query(None, ge=1, description="传则分页;不传则返回全量数组(兼容下单/选地址等)"), pageSize: Optional[int] = Query(None, ge=1, le=100, description="分页条数,默认 20"), ): """获取客户地址列表""" uid = current_user.get("user_id") if page is None: addresses = address_crud.get_by_user(db, uid) return success_response([_address_to_client_dict(a) for a in addresses]) ps = pageSize if pageSize is not None else 20 skip = (page - 1) * ps addresses, total = address_crud.get_by_user_paged(db, uid, skip=skip, limit=ps) items = [_address_to_client_dict(a) for a in addresses] total_pages = (total + ps - 1) // ps if total else 0 return success_response({ "list": items, "total": total, "page": page, "pageSize": ps, "totalPages": total_pages, }) @router.get("/addresses/{address_id}", response_model=dict) def get_address( address_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """获取地址详情""" 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_client) ): """创建地址""" 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_client) ): """更新地址""" 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_client) ): """删除地址""" 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_client) ): """设置默认地址""" 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_client) ): """获取客户个人资料""" 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, "delivery") 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_client) ): """更新客户个人资料""" 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("/price-list/current", response_model=dict) def get_current_price_list( db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """获取当前生效的价格清单""" from app.crud.price_list import price_list_crud, price_list_item_crud user = user_crud.get(db, current_user.get("user_id")) if not user or not user.region_id: return error_response(400, "用户未绑定区域") price_list = price_list_crud.get_active(db, user.region_id) if not price_list: return success_response(None) # 批量拉取价格项 + 商品/分类/单位,避免循环 N+1 items = ( db.query( BizPriceListItem.product_id, BizPriceListItem.unit_id, BizPriceListItem.price, BasProduct.name.label("product_name"), BasProduct.image_url.label("image_url"), BasProduct.category_id.label("category_id"), BasProductCategory.name.label("category_name"), BasProductUnit.name.label("unit_name"), ) .outerjoin( BasProduct, and_(BizPriceListItem.product_id == BasProduct.id, BasProduct.is_del == 0), ) .outerjoin( BasProductCategory, and_(BasProduct.category_id == BasProductCategory.id, BasProductCategory.is_del == 0), ) .outerjoin( BasProductUnit, and_(BizPriceListItem.unit_id == BasProductUnit.id, BasProductUnit.is_del == 0), ) .filter(BizPriceListItem.price_list_id == price_list.id, BizPriceListItem.is_del == 0) .all() ) # 获取关联信息 region = region_crud.get(db, price_list.region_id) region_name = region.name if region else None supplier = user_crud.get(db, price_list.supplier_id) supplier_name = supplier.real_name if supplier else None item_details = [ { "productId": item.product_id, "productName": item.product_name, "categoryName": item.category_name, "unitId": item.unit_id, "unitName": item.unit_name, "price": float(item.price) if item.price else 0, "imageUrl": item.image_url, } for item in items ] return success_response({ "id": price_list.id, "regionId": price_list.region_id, "regionName": region_name, "supplierId": price_list.supplier_id, "supplierName": supplier_name, "version": price_list.version, "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, "items": item_details }) @router.get("/price-list/products", response_model=dict) def get_price_list_products( categoryId: Optional[str] = Query(None, description="分类ID"), keyword: Optional[str] = Query(None, description="搜索关键词"), page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(20, ge=1, le=100, description="每页条数"), db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """获取价格清单商品列表(带分类和搜索)""" from app.crud.price_list import price_list_crud, price_list_item_crud from app.crud.product import product_crud from app.crud.category import category_crud from app.crud.unit import unit_crud user = user_crud.get(db, current_user.get("user_id")) if not user or not user.region_id: return error_response(400, "用户未绑定区域") price_list = price_list_crud.get_active(db, user.region_id) if not price_list: return success_response({ "list": [], "total": 0, "page": page, "pageSize": pageSize, "totalPages": 0 }) items = price_list_item_crud.get_by_price_list(db, price_list.id) # 获取商品信息并筛选 result = [] for item in items: product = product_crud.get(db, item.product_id) if not product: continue # 分类筛选 if categoryId and product.category_id != categoryId: continue # 关键词筛选 if keyword: if keyword.lower() not in (product.name or "").lower(): continue category_name = None if 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 result.append({ "productId": item.product_id, "productName": product.name, "categoryId": product.category_id, "categoryName": category_name, "unitId": item.unit_id, "unitName": unit_name, "price": float(item.price) if item.price else 0, "imageUrl": product.image_url, "description": product.description }) # 分页 total = len(result) start = (page - 1) * pageSize end = start + pageSize paginated = result[start:end] return success_response({ "list": paginated, "total": total, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize }) # ============ 订单管理 - 客户端 ============ # 订单状态映射 CLIENT_ORDER_STATUS_MAP = { "pending_payment": "待支付", "pending_review_cancel": "待审核取消", "pending_dispatch": "待分配配送", "delivering": "配送中", "delivered": "配送完成", "pending_confirm": "待确认收货", "completed": "已完结", "cancelled": "已取消" } @router.post("/orders/preview", response_model=dict) def preview_order( request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """订单预览""" from app.crud.order import order_crud from app.crud.price_list import price_list_crud, price_list_item_crud from app.crud.product import product_crud from app.crud.unit import unit_crud from decimal import Decimal payload = _resolve_request_payload(request, current_user) price_list_id = payload.get("priceListId") items = payload.get("items", []) address_id = payload.get("addressId") if not price_list_id: return error_response(400, "请选择价格清单") if not items: return error_response(400, "请选择商品") if not address_id: return error_response(400, "请选择配送地址") # 获取价格清单 price_list = price_list_crud.get(db, price_list_id) if not price_list: return error_response(404, "价格清单不存在") # 获取地址信息 address = address_crud.get(db, address_id) if not address or address.user_id != current_user.get("user_id"): return error_response(404, "地址不存在") # 获取价格清单项 price_items = price_list_item_crud.get_by_price_list(db, price_list_id) price_map = {item.product_id: item for item in price_items} # 计算金额(使用Decimal确保精度) preview_items = [] total_amount = Decimal("0") for item in items: product_id = item.get("productId") quantity = item.get("quantity", 0) price_item = price_map.get(product_id) if not price_item: return error_response(400, f"商品 {product_id} 不在价格清单中") product = product_crud.get(db, product_id) unit = unit_crud.get(db, price_item.unit_id) if price_item.unit_id else None # 使用Decimal进行精确计算 item_amount = Decimal(str(quantity)) * Decimal(str(price_item.price)) total_amount += item_amount preview_items.append({ "productId": product_id, "productName": product.name if product else None, "unitName": unit.name if unit else None, "unitPrice": float(price_item.price), "quantity": quantity, "amount": float(item_amount) }) # 拼接地址 full_address = f"{address.province or ''}{address.city or ''}{address.district or ''}{address.detail_address or ''}" return success_response({ "priceListId": price_list_id, "priceVersion": price_list.version, "items": preview_items, "totalAmount": float(total_amount), "address": { "contactPerson": address.contact_person, "contactPhone": address.contact_phone, "fullAddress": full_address } }) @router.post("/orders", response_model=dict) def create_order( request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """创建订单""" from app.crud.order import order_crud from app.crud.price_list import price_list_crud, price_list_item_crud payload = _resolve_request_payload(request, current_user) price_list_id = payload.get("priceListId") items = payload.get("items", []) address_id = payload.get("addressId") payment_method_raw = payload.get("paymentMethod", "online") payment_method = order_crud.normalize_payment_method(payment_method_raw) if payment_method not in ["online", "offline"]: return error_response(400, "支付方式仅支持线上或线下") remark = payload.get("remark") if not price_list_id: return error_response(400, "请选择价格清单") if not items: return error_response(400, "请选择商品") if not address_id: return error_response(400, "请选择配送地址") # 获取用户信息 user = user_crud.get(db, current_user.get("user_id")) if not user: return error_response(404, "用户不存在") if not user.region_id: return error_response(400, "用户未绑定区域") # 获取价格清单 price_list = price_list_crud.get(db, price_list_id) if not price_list: return error_response(404, "价格清单不存在") # 获取地址信息 address = address_crud.get(db, address_id) if not address or address.user_id != current_user.get("user_id"): return error_response(404, "地址不存在") # 获取价格清单项 price_items = price_list_item_crud.get_by_price_list(db, price_list_id) price_map = {item.product_id: item for item in price_items} # 验证商品并整理订单项 order_items = [] for item in items: product_id = item.get("productId") quantity = item.get("quantity", 0) unit_id = item.get("unitId") price_item = price_map.get(product_id) if not price_item: return error_response(400, f"商品不在价格清单中") order_items.append({ "productId": product_id, "unitId": unit_id or price_item.unit_id, "quantity": quantity, "unitPrice": float(price_item.price) }) # 拼接地址 full_address = f"{address.province or ''}{address.city or ''}{address.district or ''}{address.detail_address or ''}" creator = { "user_id": current_user.get("user_id"), "real_name": current_user.get("real_name") } order = order_crud.create_order( db, client_id=current_user.get("user_id"), region_id=user.region_id, price_list_id=price_list_id, price_version=price_list.version, items=order_items, address_id=address_id, address_info={ "contactPerson": address.contact_person, "contactPhone": address.contact_phone, "fullAddress": full_address }, payment_method=payment_method, remark=remark, creator=creator ) order_status = getattr(order, "order_status", None) or "pending_confirm" delivery_status = getattr(order, "delivery_status", None) or "pending_dispatch" pay_status = getattr(order, "pay_status", None) or "pending_payment" return success_response({ "orderId": order.id, "orderNo": order.order_no, "totalAmount": float(order.total_amount), "status": order.status, "orderStatus": order_status, "deliveryStatus": delivery_status, "payStatus": pay_status, "paymentMethod": order_crud.normalize_payment_method(order.payment_method), }) @router.get("/orders", response_model=dict) def list_client_orders( status: Optional[str] = Query(None, description="订单状态(兼容旧参数;与 listTab 同时传时以 listTab 为准)"), orderStatus: Optional[str] = Query(None, description="订单主状态:pending_payment/pending_confirm/completed/cancelled"), listTab: Optional[str] = Query( None, description="列表分段:all / pending_payment / pending_confirm / completed / cancelled,用于 Tab 独立分页", ), 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_client) ): """获取客户订单列表""" from app.crud.order import order_crud, order_item_crud segment = (listTab or "all").strip().lower() if segment not in ("all", "pending_payment", "pending_confirm", "completed", "cancelled"): segment = "all" use_status = status if segment == "all" else None use_list_tab = segment if segment != "all" else None use_order_status = (orderStatus or "").strip() if segment == "all" else None orders, total = order_crud.get_multi_with_filters( db, client_id=current_user.get("user_id"), status=use_status, order_status=use_order_status or None, list_tab=use_list_tab, skip=(page - 1) * pageSize, limit=pageSize ) order_ids = [o.id for o in orders] # 批量获取订单项(用于订单列表中展示商品列表) items_by_order_id: dict[str, list[BizOrderItem]] = {} product_map: dict[str, BasProduct] = {} if order_ids: all_items: list[BizOrderItem] = db.query(BizOrderItem).filter( BizOrderItem.is_del == 0, BizOrderItem.order_id.in_(order_ids) ).all() for it in all_items: items_by_order_id.setdefault(it.order_id, []).append(it) product_ids = list({it.product_id for it in all_items if it.product_id}) if product_ids: products: list[BasProduct] = db.query(BasProduct).filter( BasProduct.is_del == 0, BasProduct.id.in_(product_ids) ).all() product_map = {p.id: p for p in products} result = [] for order in orders: order_status = getattr(order, "order_status", None) or "pending_confirm" delivery_status = getattr(order, "delivery_status", None) or "pending_dispatch" pay_status = getattr(order, "pay_status", None) or "pending_payment" order_items = items_by_order_id.get(order.id, []) item_count = len(order_items) if order_items else order_item_crud.get_count_by_order(db, order.id) total_quantity = float(sum([(it.quantity or 0) for it in order_items])) if order_items else None item_details = [] for it in order_items: p = product_map.get(it.product_id) item_details.append({ "id": it.id, "productId": it.product_id, "productName": (p.name if p else None), "quantity": float(it.quantity) if it.quantity else 0, "imageUrl": (p.image_url if p else None), }) result.append({ "id": order.id, "orderNo": order.order_no, "totalAmount": float(order.total_amount) if order.total_amount else 0, "paidAmount": float(order.paid_amount) if order.paid_amount else 0, "status": order.status, "orderStatus": order_status, "deliveryStatus": delivery_status, "payStatus": pay_status, "paymentMethod": order_crud.normalize_payment_method(order.payment_method), "itemCount": item_count, "totalQuantity": total_quantity if total_quantity is not None else item_count, "items": item_details, "createAt": str(order.create_at) if order.create_at else None }) return success_response({ "list": result, "total": total, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize }) @router.get("/orders/{order_id}", response_model=dict) def get_client_order( order_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """获取客户订单详情""" from app.crud.order import order_crud, order_item_crud, order_status_log_crud from app.crud.product import product_crud from app.crud.unit import unit_crud from app.crud.deliverer_review import deliverer_review_crud order = order_crud.get(db, order_id) if not order: return error_response(404, "订单不存在") if order.client_id != current_user.get("user_id"): return error_response(403, "无权查看此订单") # 获取订单项 items = order_item_crud.get_by_order(db, order_id) # 配送信息与评价信息 from app.crud.deliverer_review import deliverer_review_crud delivery_task = db.query(BizDeliveryTask).filter( BizDeliveryTask.order_id == order_id, BizDeliveryTask.is_del == 0 ).first() deliverer_name = None deliverer_phone = None if delivery_task and delivery_task.deliver_id: du = db.query(SysUser).filter( SysUser.id == delivery_task.deliver_id, SysUser.is_del == 0, ).first() if du: deliverer_name = du.real_name or du.username deliverer_phone = _deliverer_contact_phone(du) existing_review = deliverer_review_crud.get_by_order(db, order_id) # 获取状态日志 status_logs = order_status_log_crud.get_by_order(db, order_id) # 获取区域名称 region_name = None if order.region_id: region = region_crud.get(db, order.region_id) region_name = region.name if region else None # 处理订单项 item_details = [] for item in items: product = product_crud.get(db, item.product_id) unit = unit_crud.get(db, item.unit_id) if item.unit_id else None item_details.append({ "id": item.id, "productId": item.product_id, "productName": product.name if product else None, "unitId": item.unit_id, "unitName": unit.name if unit else None, "measureValue": float(product.measure_value) if (product and getattr(product, "measure_value", None) is not None) else None, "quantity": float(item.quantity) if item.quantity else 0, "unitPrice": float(item.unit_price) if item.unit_price else 0, "amount": float(item.amount) if item.amount else 0, "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0, "status": item.status, "imageUrl": product.image_url if product else None, }) # 处理状态日志 log_details = [] for log in status_logs: log_details.append({ "status": log.to_status, "statusText": CLIENT_ORDER_STATUS_MAP.get(log.to_status, log.to_status), # BizOrderStatusLog 无 operator_name 字段;优先用 create_name(通用审计字段) "operatorName": getattr(log, "create_name", None) or "", "operatorRole": log.operator_role, "reason": log.reason, "createAt": str(log.create_at) if log.create_at else None }) from app.core.order_status_compat import derive_three_status_from_legacy ts = derive_three_status_from_legacy(order.status, paid_amount=order.paid_amount) order_status = getattr(order, "order_status", None) or ts.order_status delivery_status = getattr(order, "delivery_status", None) or ts.delivery_status pay_status = getattr(order, "pay_status", None) or ts.pay_status return success_response({ "id": order.id, "orderNo": order.order_no, "regionId": order.region_id, "regionName": region_name, "priceListId": order.price_list_id, "priceVersion": order.price_version, "status": order.status, "orderStatus": order_status, "deliveryStatus": delivery_status, "payStatus": pay_status, "paymentMethod": order_crud.normalize_payment_method(order.payment_method), "totalAmount": float(order.total_amount) if order.total_amount else 0, "paidAmount": float(order.paid_amount) if order.paid_amount else 0, "receivableAmount": float(order.receivable_amount) if order.receivable_amount else 0, "refundAmount": float(order.refund_amount) if order.refund_amount else 0, "contactPerson": order.contact_person, "contactPhone": order.contact_phone, "deliveryAddress": order.delivery_address, "remark": order.remark, "cancelReason": order.cancel_reason, "createAt": str(order.create_at) if order.create_at else None, "paymentTime": str(order.payment_time) if order.payment_time else None, "deliveryAssignTime": str(getattr(order, "delivery_assign_time", None)) if getattr(order, "delivery_assign_time", None) else None, "deliveredTime": str(getattr(order, "delivered_time", None)) if getattr(order, "delivered_time", None) else None, "delivererId": delivery_task.deliver_id if delivery_task else None, "delivererName": deliverer_name, "delivererPhone": deliverer_phone, "canReviewDeliverer": order.status == "completed" and delivery_task is not None and not existing_review, "delivererReview": { "id": existing_review.id, "score": existing_review.score, "tags": (existing_review.tags or "").split(",") if existing_review and existing_review.tags else [], "content": existing_review.content, "createAt": str(existing_review.create_at) if existing_review and existing_review.create_at else None } if existing_review else None, "items": item_details, "statusLogs": log_details }) @router.post("/orders/{order_id}/deliverer-review", response_model=dict) def review_deliverer( order_id: str, request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """客户评价配送员""" from app.crud.order import order_crud from app.crud.deliverer_review import deliverer_review_crud order = order_crud.get(db, order_id) if not order: return error_response(404, "订单不存在") if order.client_id != current_user.get("user_id"): return error_response(403, "无权操作此订单") if order.status != "completed": return error_response(400, "仅已完成订单可评价") task = db.query(BizDeliveryTask).filter( BizDeliveryTask.order_id == order_id, BizDeliveryTask.is_del == 0 ).first() if not task or not task.deliver_id: return error_response(400, "未找到对应配送员") exists = deliverer_review_crud.get_by_order(db, order_id) if exists: return error_response(400, "该订单已评价") payload = _resolve_request_payload(request, current_user) score = int(payload.get("score", 0)) if score < 1 or score > 5: return error_response(400, "评分必须在1-5之间") tags = payload.get("tags") or [] content = payload.get("content") is_anonymous = bool(payload.get("isAnonymous", False)) creator = { "user_id": current_user.get("user_id"), "real_name": current_user.get("real_name") } review = deliverer_review_crud.create_review( db=db, order_id=order_id, task_id=task.id, client_id=current_user.get("user_id"), deliverer_id=task.deliver_id, score=score, tags=tags, content=content, is_anonymous=is_anonymous, creator=creator ) return success_response({ "id": review.id, "score": review.score, "message": "评价成功" }) @router.post("/orders/{order_id}/cancel", response_model=dict) def client_cancel_order( order_id: str, request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """客户发起取消申请 — 产品策略:客户端不可自助取消,请联系客服。""" return error_response(403, "订单取消请联系客服人员处理") @router.post("/orders/{order_id}/direct-cancel", response_model=dict) def client_direct_cancel_order( order_id: str, request: Optional[dict] = None, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """客户直接取消未支付订单 — 产品策略:客户端不可自助取消,请联系客服或等待超时自动关闭。""" return error_response(403, "订单取消请联系客服人员处理") @router.post("/orders/{order_id}/confirm-receive", response_model=dict) def client_confirm_receive( order_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """客户确认收货""" from app.crud.order import order_crud order = order_crud.get(db, order_id) if not order: return error_response(404, "订单不存在") if order.client_id != current_user.get("user_id"): return error_response(403, "无权操作此订单") order = order_crud.confirm_receive( db, order_id=order_id, operator_id=current_user.get("user_id") ) if not order: return error_response(400, "当前状态不支持确认收货") return success_response({ "id": order.id, "status": order.status, "orderStatus": getattr(order, "order_status", None) or "pending_confirm", "deliveryStatus": getattr(order, "delivery_status", None) or "pending_dispatch", "payStatus": getattr(order, "pay_status", None) or "pending_payment", "message": "确认收货成功" }) @router.get("/orders/{order_id}/delivery-progress", response_model=dict) def get_delivery_progress( order_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """查看配送进度""" from app.crud.order import order_crud, order_item_crud, order_status_log_crud from app.crud.product import product_crud _dt_title = { "dt_supplier_stocking": "供应商开始备货", "dt_supplier_ready": "供应商备货完成(待配送员取货)", "dt_picking": "配送员取货中", "dt_delivering": "配送员配送中", "dt_completed": "配送任务已完成", "dt_assigned": "配送任务已分配", "dt_pending": "配送任务待分配", "dt_cancelled": "配送任务已取消", } order = order_crud.get(db, order_id) if not order: return error_response(404, "订单不存在") if order.client_id != current_user.get("user_id"): return error_response(403, "无权查看此订单") # 获取订单项 items = order_item_crud.get_by_order(db, order_id) item_details = [] for item in items: product = product_crud.get(db, item.product_id) item_details.append({ "productId": item.product_id, "productName": product.name if product else None, "quantity": float(item.quantity) if item.quantity else 0, "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0, "status": item.status }) status_logs = order_status_log_crud.get_by_order(db, order_id) task_events = [] for log in status_logs: ts = log.to_status or "" if not str(ts).startswith("dt_"): continue task_events.append({ "title": _dt_title.get(ts, ts), "remark": log.reason, "operatorRole": log.operator_role, "createAt": str(log.create_at) if log.create_at else None, }) task_events.sort(key=lambda x: x.get("createAt") or "") return success_response({ "orderId": order.id, "orderNo": order.order_no, "status": order.status, "orderStatus": getattr(order, "order_status", None), "deliveryStatus": getattr(order, "delivery_status", None), "payStatus": getattr(order, "pay_status", None), "items": item_details, "deliveryTaskEvents": task_events, }) @router.post("/orders/{order_id}/urge", response_model=dict) def client_urge_order( order_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """客户催单""" from app.crud.order import order_crud from app.models.models import BizDeliveryTask from datetime import datetime, timedelta order = order_crud.get(db, order_id) if not order: return error_response(404, "订单不存在") if order.client_id != current_user.get("user_id"): return error_response(403, "无权操作此订单") # 检查订单状态,只有配送中的订单才能催单 if getattr(order, "delivery_status", None) != "delivering": return error_response(400, "当前状态不支持催单") # 获取今天已催单次数(从订单备注中统计) today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) tomorrow = today + timedelta(days=1) # 查询今天的催单次数 urge_count = 1 # 默认允许催单 last_urge_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 获取客户信息 client = user_crud.get(db, current_user.get("user_id")) client_name = client.real_name if client else "未知客户" # 获取配送员信息 deliver_name = None deliver_phone = None task = db.query(BizDeliveryTask).filter( BizDeliveryTask.order_id == order_id, BizDeliveryTask.is_del == 0 ).first() if task and task.deliver_id: deliver = user_crud.get(db, task.deliver_id) if deliver: deliver_name = deliver.real_name deliver_phone = deliver.phone # 发送消息通知配送员 try: from app.api.message import message_crud from app.core.utils import generate_id from app.models.models import BizMessage # 构造催单内容 urge_content = f"【催单提醒】客户 {client_name} 催单,订单号:{order.order_no},联系电话:{client.phone or '无'}。请尽快处理!" # 发送给配送员 if task and task.deliver_id: message_data = { "id": generate_id(), "receiver_id": task.deliver_id, "receiver_name": deliver_name, "message_type": "delivery", "title": f"【催单提醒】订单 {order.order_no}", "content": urge_content, "is_read": 0, "create_by": current_user.get("user_id"), "create_name": client_name } message_obj = BizMessage(**message_data) db.add(message_obj) # 同时通知运营人员(查找所有admin角色用户) admin_users = db.query(SysUser).filter( SysUser.role == "admin", SysUser.is_active == True, SysUser.is_del == 0 ).all() for admin in admin_users: admin_message_data = { "id": generate_id(), "receiver_id": admin.id, "receiver_name": admin.real_name, "message_type": "order", "title": f"【客户催单】订单 {order.order_no}", "content": urge_content, "is_read": 0, "create_by": current_user.get("user_id"), "create_name": client_name } admin_message = BizMessage(**admin_message_data) db.add(admin_message) db.commit() except Exception as e: # 消息发送失败不影响催单主流程,只记录日志 import logging logging.warning(f"发送催单消息失败: {str(e)}") return success_response({ "orderId": order.id, "urgeCount": urge_count, "lastUrgeTime": last_urge_time, "message": "催单成功,配送员将尽快处理" }) # ============ 结算与退款 - 客户端 ============ @router.get("/settlements", response_model=dict) def list_client_settlements( 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_client) ): """获取我的结算记录列表""" from app.crud.settlement import settlement_crud from app.models.models import BizOrder client_id = current_user.get("user_id") # 获取该客户的所有结算记录 settlements, total = settlement_crud.get_multi_with_filters( db, client_id=client_id, status="confirmed", skip=(page - 1) * pageSize, limit=pageSize ) result = [] for settlement in settlements: order = db.query(BizOrder).filter(BizOrder.id == settlement.order_id).first() result.append({ "id": settlement.id, "orderId": settlement.order_id, "orderNo": order.order_no if order else None, "amount": float(settlement.amount) if settlement.amount else 0, "settlementMethod": settlement.settlement_method, "settlementTime": str(settlement.settlement_time) if settlement.settlement_time else None, "settlementNote": settlement.settlement_note, "createAt": str(settlement.create_at) if settlement.create_at else None }) return success_response({ "list": result, "total": total, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize }) @router.get("/settlements/{settlement_id}", response_model=dict) def get_client_settlement( settlement_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """获取结算记录详情""" from app.crud.settlement import settlement_crud from app.models.models import BizOrder client_id = current_user.get("user_id") settlement = settlement_crud.get(db, settlement_id) if not settlement: return error_response(404, "结算记录不存在") if settlement.client_id != client_id: return error_response(403, "无权查看此记录") order = db.query(BizOrder).filter(BizOrder.id == settlement.order_id).first() return success_response({ "id": settlement.id, "orderId": settlement.order_id, "orderNo": order.order_no if order else None, "amount": float(settlement.amount) if settlement.amount else 0, "settlementMethod": settlement.settlement_method, "settlementTime": str(settlement.settlement_time) if settlement.settlement_time else None, "settlementNote": settlement.settlement_note, "settlementFileUrl": settlement.settlement_file_url, "reviewedBy": settlement.reviewed_by, "reviewedTime": str(settlement.reviewed_time) if settlement.reviewed_time else None, "createAt": str(settlement.create_at) if settlement.create_at else None }) @router.get("/settlements/statistics", response_model=dict) def get_client_settlement_statistics( db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """获取我的结算统计""" from app.crud.settlement import settlement_crud from app.models.models import BizOrder, BizSettlement from sqlalchemy import func client_id = current_user.get("user_id") # 获取已结算总额 result = db.query( func.sum(BizSettlement.amount) ).filter( BizSettlement.client_id == client_id, BizSettlement.status == "confirmed", BizSettlement.is_del == 0 ).scalar() or 0 return success_response({ "totalSettled": float(result) }) @router.get("/refunds", response_model=dict) def list_client_refunds( 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_client) ): """获取我的退款记录列表""" from app.crud.settlement import refund_crud from app.models.models import BizOrder client_id = current_user.get("user_id") # 获取该客户的所有退款记录(关联订单) refunds, total = refund_crud.get_multi_with_filters( db, skip=(page - 1) * pageSize, limit=pageSize ) result = [] for refund in refunds: order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first() if order and order.client_id == client_id: result.append({ "id": refund.id, "orderId": refund.order_id, "orderNo": order.order_no if order else None, "refundNo": refund.refund_no, "refundAmount": float(refund.refund_amount) if refund.refund_amount else 0, "refundMethod": refund.refund_method, "refundResult": refund.refund_result, "status": refund.status, "refundTime": str(refund.refund_time) if refund.refund_time else None, "refundFailReason": refund.refund_fail_reason, "createAt": str(refund.create_at) if refund.create_at else None }) return success_response({ "list": result, "total": len(result), "page": page, "pageSize": pageSize, "totalPages": (len(result) + pageSize - 1) // pageSize }) @router.get("/refunds/{refund_id}", response_model=dict) def get_client_refund( refund_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """获取退款详情""" from app.crud.settlement import refund_crud from app.models.models import BizOrder client_id = current_user.get("user_id") refund = refund_crud.get(db, refund_id) if not refund: return error_response(404, "退款记录不存在") order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first() if order and order.client_id != client_id: return error_response(403, "无权查看此记录") return success_response({ "id": refund.id, "orderId": refund.order_id, "orderNo": order.order_no if order else None, "refundNo": refund.refund_no, "refundAmount": float(refund.refund_amount) if refund.refund_amount else 0, "refundMethod": refund.refund_method, "refundResult": refund.refund_result, "refundTime": str(refund.refund_time) if refund.refund_time else None, "refundNoWechat": refund.refund_no_wechat, "refundFailReason": refund.refund_fail_reason, "status": refund.status, "createAt": str(refund.create_at) if refund.create_at else None, "createName": refund.create_name }) @router.post("/refunds/apply/{order_id}", response_model=dict) def apply_client_refund( order_id: str, request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """申请退款(线下冲销)""" from app.crud.settlement import refund_crud from app.models.models import BizOrder from decimal import Decimal client_id = current_user.get("user_id") order = db.query(BizOrder).filter(BizOrder.id == order_id).first() if not order: return error_response(404, "订单不存在") if order.client_id != client_id: return error_response(403, "无权操作此订单") payload = _resolve_request_payload(request, current_user) refund_amount = payload.get("refundAmount") reason = payload.get("reason") if not refund_amount or refund_amount <= 0: return error_response(400, "退款金额必须大于0") # 检查已支付金额 paid_amount = float(order.paid_amount) if order.paid_amount else 0 if refund_amount > paid_amount: return error_response(400, "退款金额不能超过已支付金额") # 检查是否有处理中的退款 existing_refunds = refund_crud.get_by_order(db, order_id) processing_refund = any(r.status in ["pending", "processing"] for r in existing_refunds) if processing_refund: return error_response(400, "该订单有退款正在处理中,请勿重复提交") creator = { "user_id": current_user.get("user_id"), "real_name": current_user.get("real_name") } refund = refund_crud.create_refund( db, order_id=order_id, refund_amount=Decimal(str(refund_amount)), refund_method="offline", reason=reason, creator=creator ) return success_response({ "refundId": refund.id, "refundNo": refund.refund_no, "refundAmount": float(refund.refund_amount), "status": refund.status, "message": "退款申请已提交" }) @router.get("/refunds/statistics", response_model=dict) def get_client_refund_statistics( db: Session = Depends(get_db), current_user: dict = Depends(require_client) ): """获取我的退款统计""" from app.crud.settlement import refund_crud from app.models.models import BizOrder from sqlalchemy import func client_id = current_user.get("user_id") # 获取所有退款记录 refunds = refund_crud.get_multi_with_filters(db, limit=1000)[0] # 筛选属于该客户的 client_refunds = [] for refund in refunds: order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first() if order and order.client_id == client_id: client_refunds.append(refund) total_refunds = len(client_refunds) success_refunds = sum(1 for r in client_refunds if r.status == "completed") pending_refunds = sum(1 for r in client_refunds if r.status in ["pending", "processing"]) failed_refunds = sum(1 for r in client_refunds if r.status == "failed") success_amount = sum( float(r.refund_amount) for r in client_refunds if r.status == "completed" and r.refund_amount ) return success_response({ "totalRefunds": total_refunds, "successRefunds": success_refunds, "pendingRefunds": pending_refunds, "failedRefunds": failed_refunds, "totalRefunded": success_amount })