"""配送管理接口""" from typing import Optional, List from datetime import datetime from decimal import Decimal from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from sqlalchemy import and_, desc, func from app.core.database import get_db from app.core.auth import get_current_user from app.core.response import success_response, error_response from app.crud.region import region_crud from app.crud.user import user_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.crud.address import address_crud from app.core.order_status_compat import derive_legacy_status_from_three from app.models.models import BizPriceListItem, SysRegion, SysUser, BizOrder, BizDeliveryTaskItem, BasAddress from app.api.admin import _resolve_admin_payload_with_crypto_fallback router = APIRouter(prefix="/web", tags=["运营端-配送管理"]) def _numeric_to_float_optional(v) -> Optional[float]: if v is None: return None return float(v) def _category_path_display(db: Session, category_id: Optional[str]) -> str: """一级/二级分类用 / 连接(与订单详情一致)。""" from app.crud.category import category_crud if not category_id: return "" names: List[str] = [] cid = str(category_id).strip() seen = set() depth = 0 while cid and depth < 24 and cid not in seen: seen.add(cid) cat = category_crud.get(db, cid) if not cat: break names.insert(0, (getattr(cat, "name", None) or "").strip()) pid = getattr(cat, "parent_id", None) cid = str(pid).strip() if pid else "" depth += 1 return "/".join([n for n in names if n]) def _supplier_unit_yuan_from_pli(pli: Optional[BizPriceListItem]) -> 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 _price_list_item_map(db: Session, price_list_ids: List[str]) -> dict: """(price_list_id, product_id, unit_id) -> BizPriceListItem""" if not price_list_ids: return {} rows = ( db.query(BizPriceListItem) .filter(BizPriceListItem.price_list_id.in_(price_list_ids), BizPriceListItem.is_del == 0) .all() ) return {(r.price_list_id, r.product_id, r.unit_id): r for r in rows} def _supplier_pickup_address_block(db: Session, supplier_id: Optional[str]) -> dict: """供应商默认取货地址;无默认时取第一条 pickup 类型地址。""" empty = {"fullAddress": None, "contactPerson": None, "contactPhone": None} if not supplier_id: return empty addr = address_crud.get_default(db, supplier_id, "pickup") if not addr: rows = address_crud.get_by_user_and_type(db, supplier_id, "pickup") addr = rows[0] if rows else None if not addr: return empty segs = [ getattr(addr, "province", None), getattr(addr, "city", None), getattr(addr, "district", None), getattr(addr, "detail_address", None), ] full = "".join(s for s in segs if s) return { "fullAddress": full or None, "contactPerson": addr.contact_person, "contactPhone": addr.contact_phone, } def _batch_supplier_pickup_address_map(db: Session, supplier_ids: List[str]) -> dict: """批量获取供应商取货地址,避免列表接口逐条查询。""" if not supplier_ids: return {} rows = ( db.query(BasAddress) .filter( BasAddress.user_id.in_(supplier_ids), BasAddress.address_type == "pickup", BasAddress.is_del == 0, ) .order_by(BasAddress.user_id.asc(), BasAddress.is_default.desc(), BasAddress.create_at.desc(), BasAddress.id.desc()) .all() ) addr_map = {} for addr in rows: uid = str(getattr(addr, "user_id", "") or "") if not uid or uid in addr_map: continue segs = [ getattr(addr, "province", None), getattr(addr, "city", None), getattr(addr, "district", None), getattr(addr, "detail_address", None), ] full = "".join(s for s in segs if s) addr_map[uid] = { "fullAddress": full or None, "contactPerson": getattr(addr, "contact_person", None), "contactPhone": getattr(addr, "contact_phone", None), } return addr_map def require_admin(current_user: dict = Depends(get_current_user)): """验证运营人员权限""" if current_user.get("role") not in {"admin", "super_admin"}: return error_response(403, "需要运营人员权限") return current_user # 配送任务状态映射 DELIVERY_STATUS_MAP = { "pending": "待取货", "assigned": "已分配", "picking": "取货中", "delivering": "配送中", "completed": "已完成", "cancelled": "已取消" } @router.get("/delivery-tasks", response_model=dict) def list_delivery_tasks( page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(10, ge=1, le=100, description="每页条数"), regionId: Optional[str] = Query(None, description="区域ID"), status: Optional[str] = Query(None, description="状态"), orderNo: Optional[str] = Query(None, description="订单号(兼容:仅按配送单号 task_no 模糊匹配)"), keyword: Optional[str] = Query(None, description="关键词:配送单号/订单编号/联系人/电话等(与运营端筛选一致)"), db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """获取配送任务列表""" from app.crud.delivery import delivery_task_crud tasks, total = delivery_task_crud.get_multi_with_filters( db, region_id=regionId, status=status, order_no=orderNo, keyword=keyword, skip=(page - 1) * pageSize, limit=pageSize ) region_map = {} supplier_map = {} deliver_map = {} client_map = {} task_ids = [t.id for t in tasks if getattr(t, "id", None)] region_ids = list({t.region_id for t in tasks if getattr(t, "region_id", None)}) supplier_ids = list({t.supplier_id for t in tasks if getattr(t, "supplier_id", None)}) deliver_ids = list({t.deliver_id for t in tasks if getattr(t, "deliver_id", None)}) order_ids = list({t.order_id for t in tasks if getattr(t, "order_id", None)}) # 批量加载区域 if region_ids: regions = db.query(SysRegion).filter(SysRegion.id.in_(region_ids), SysRegion.is_del == 0).all() region_map = {r.id: r.name for r in regions} # 批量加载供应商/配送员 user_ids = list({*supplier_ids, *deliver_ids}) if user_ids: users = db.query(SysUser).filter(SysUser.id.in_(user_ids), SysUser.is_del == 0).all() user_map = {u.id: u for u in users} for sid in supplier_ids: u = user_map.get(sid) supplier_map[sid] = {"name": u.real_name if u else None, "phone": u.phone if u else None} for did in deliver_ids: u = user_map.get(did) deliver_map[did] = {"name": u.real_name if u else None, "phone": u.phone if u else None} # 批量加载订单与客户 order_map = {} if order_ids: orders = db.query(BizOrder).filter(BizOrder.id.in_(order_ids), BizOrder.is_del == 0).all() order_map = {o.id: o for o in orders} client_ids = list({o.client_id for o in orders if getattr(o, "client_id", None)}) if client_ids: clients = db.query(SysUser).filter(SysUser.id.in_(client_ids), SysUser.is_del == 0).all() client_map = {u.id: {"name": u.real_name if u else None, "phone": u.phone if u else None} for u in clients} # 批量聚合任务明细数量 item_count_map = {} if task_ids: rows = ( db.query(BizDeliveryTaskItem.task_id, func.count(BizDeliveryTaskItem.id)) .filter(BizDeliveryTaskItem.task_id.in_(task_ids), BizDeliveryTaskItem.is_del == 0) .group_by(BizDeliveryTaskItem.task_id) .all() ) item_count_map = {task_id: int(cnt or 0) for task_id, cnt in rows} # 批量供应商取货地址 pickup_addr_map = _batch_supplier_pickup_address_map(db, supplier_ids) result = [] for task in tasks: # 获取订单信息 order_info = {} if task.order_id: order = order_map.get(task.order_id) if order: order_info = { "orderNo": order.order_no, "totalAmount": float(order.total_amount) if order.total_amount else 0, "clientId": order.client_id, "clientName": client_map.get(order.client_id, {}).get("name") if order.client_id else None, "contactPerson": order.contact_person, "contactPhone": order.contact_phone, "deliveryAddress": order.delivery_address } item_count = item_count_map.get(task.id, 0) # 供应商取货地址(用于列表展示) supplier_pickup = pickup_addr_map.get(task.supplier_id, {"fullAddress": None, "contactPerson": None, "contactPhone": None}) result.append({ "id": task.id, "taskNo": task.task_no, "orderId": task.order_id, "orderNo": order_info.get("orderNo"), "regionId": task.region_id, "regionName": region_map.get(task.region_id), "supplierId": task.supplier_id, "supplierName": supplier_map.get(task.supplier_id, {}).get("name"), "supplierAddress": supplier_pickup.get("fullAddress"), "deliverId": task.deliver_id, "deliverName": deliver_map.get(task.deliver_id, {}).get("name"), "deliverPhone": deliver_map.get(task.deliver_id, {}).get("phone"), "status": task.status, "totalAmount": order_info.get("totalAmount"), "itemCount": item_count, "clientId": order_info.get("clientId"), "clientName": order_info.get("clientName"), "contactPerson": order_info.get("contactPerson"), "contactPhone": order_info.get("contactPhone"), "deliveryAddress": order_info.get("deliveryAddress"), "dispatchRemark": task.dispatch_remark, "createAt": str(task.create_at) if task.create_at else None, "assignTime": str(task.create_at) if task.deliver_id else None, "pickupTime": str(task.pickup_time) if task.pickup_time else None, "deliveryTime": str(task.delivery_time) if task.delivery_time else None, "expireTime": str(task.effective_time) if task.effective_time else None, "isExpiring": False }) return success_response({ "list": result, "total": total, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize }) @router.get("/delivery-tasks/{task_id}", response_model=dict) def get_delivery_task( task_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """获取配送任务详情""" from app.crud.delivery import delivery_task_crud, delivery_task_item_crud task = delivery_task_crud.get(db, task_id) if not task: return error_response(404, "配送任务不存在") # 获取任务明细 items = delivery_task_item_crud.get_by_task(db, task_id) # 获取关联信息 region_name = None if task.region_id: region = region_crud.get(db, task.region_id) region_name = region.name if region else None supplier_info = {"name": None, "phone": None} if task.supplier_id: supplier = user_crud.get(db, task.supplier_id) if supplier: supplier_info = {"name": supplier.real_name, "phone": supplier.phone} deliver_info = {"name": None, "phone": None} if task.deliver_id: deliver = user_crud.get(db, task.deliver_id) if deliver: deliver_info = {"name": deliver.real_name, "phone": deliver.phone} # 获取订单信息 order_info = {} order_price_list_id: Optional[str] = None if task.order_id: order = order_crud.get(db, task.order_id) if order: order_info = { "orderNo": order.order_no, "totalAmount": float(order.total_amount) if order.total_amount else 0, "contactPerson": order.contact_person, "contactPhone": order.contact_phone, "deliveryAddress": order.delivery_address } order_price_list_id = getattr(order, "price_list_id", None) pickup_addr = _supplier_pickup_address_block(db, task.supplier_id) # 处理任务明细(商品展示字段对齐运营端订单明细) item_details = [] pli_map = _price_list_item_map(db, [order_price_list_id]) if order_price_list_id else {} for item in items: product = product_crud.get(db, item.product_id) order_line = order_item_crud.get(db, item.order_item_id) if item.order_item_id else None unit_row = None if order_line and order_line.unit_id: unit_row = unit_crud.get(db, order_line.unit_id) elif product and product.unit_id: unit_row = unit_crud.get(db, product.unit_id) image_url = getattr(product, "image_url", None) if product else None category_path = "" if product and getattr(product, "category_id", None): category_path = _category_path_display(db, str(product.category_id)) measure_value = _numeric_to_float_optional(getattr(product, "measure_value", None)) if product else None qty = float(item.quantity) if item.quantity else 0.0 unit_price = float(order_line.unit_price) if order_line and order_line.unit_price is not None else None amount = float(order_line.amount) if order_line and order_line.amount is not None else None if amount is None and unit_price is not None: amount = round(qty * float(unit_price), 2) client = user_crud.get(db, item.client_id) if item.client_id else None # 供货/售价字段:对齐「订单详情」逻辑:优先使用价格清单计算供货单价;无价格清单时兜底订单行售价 selling_unit_price = unit_price selling_amount = amount unit_id_for_price = None if order_line and getattr(order_line, "unit_id", None): unit_id_for_price = order_line.unit_id elif product and getattr(product, "unit_id", None): unit_id_for_price = product.unit_id supplier_unit_price = None if order_price_list_id and item.product_id and unit_id_for_price: pli = pli_map.get((order_price_list_id, item.product_id, unit_id_for_price)) supplier_unit_price = _supplier_unit_yuan_from_pli(pli) if supplier_unit_price is None: supplier_unit_price = unit_price supplier_amount = None if supplier_unit_price is not None: supplier_amount = round(qty * float(supplier_unit_price), 2) item_details.append({ "id": item.id, "orderItemId": item.order_item_id, "productId": item.product_id, "productName": product.name if product else None, "imageUrl": image_url, "categoryName": category_path or None, "measureValue": measure_value, "unitName": unit_row.name if unit_row else None, "unitPrice": unit_price, "amount": amount, "sellingUnitPrice": selling_unit_price, "sellingAmount": selling_amount, "supplierId": task.supplier_id, "supplierName": supplier_info.get("name"), "supplierUnitPrice": supplier_unit_price, "supplierAmount": supplier_amount, "quantity": qty, "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0, "clientName": client.real_name if client else None, "clientPhone": client.phone if client else None, "clientAddress": item.delivery_address, "contactPerson": item.contact_person, "contactPhone": item.contact_phone, "deliveryAddress": item.delivery_address, "status": item.status }) # 凭证图片:与运营端订单详情一致(附件表优先,任务字段兜底) from app.crud.attachment import attachment_crud as _attachment_crud prepare_images = [ a.file_url for a in _attachment_crud.get_by_business(db, "delivery_prepare", task.id) if a.file_url ] pickup_images = [ a.file_url for a in _attachment_crud.get_by_business(db, "delivery_pickup", task.id) if a.file_url ] delivery_images = [ a.file_url for a in _attachment_crud.get_by_business(db, "delivery_deliver", task.id) if a.file_url ] if not pickup_images and getattr(task, "pickup_image_url", None): pickup_images = [ s.strip() for s in str(task.pickup_image_url).split(",") if s and str(s).strip() ] if not delivery_images and getattr(task, "delivery_image_url", None): delivery_images = [ s.strip() for s in str(task.delivery_image_url).split(",") if s and str(s).strip() ] return success_response({ "id": task.id, "taskNo": task.task_no, "orderId": task.order_id, "orderNo": order_info.get("orderNo"), "regionId": task.region_id, "regionName": region_name, "supplierId": task.supplier_id, "supplierName": supplier_info.get("name"), "supplierPhone": supplier_info.get("phone"), "supplierAddress": pickup_addr.get("fullAddress"), "supplierPickup": { "address": pickup_addr.get("fullAddress"), "contactPerson": pickup_addr.get("contactPerson"), "contactPhone": pickup_addr.get("contactPhone"), }, "customerDelivery": { "address": order_info.get("deliveryAddress"), "contactPerson": order_info.get("contactPerson"), "contactPhone": order_info.get("contactPhone"), }, "deliverId": task.deliver_id, "deliverName": deliver_info.get("name"), "deliverPhone": deliver_info.get("phone"), "status": task.status, "totalAmount": order_info.get("totalAmount"), "createAt": str(task.create_at) if task.create_at else None, "assignTime": str(task.create_at) if task.deliver_id else None, "pickupTime": str(task.pickup_time) if task.pickup_time else None, "pickupRemark": task.pickup_remark, "prepareImages": prepare_images, "pickupImages": pickup_images, "deliveryTime": str(task.delivery_time) if task.delivery_time else None, "deliveryRemark": task.delivery_remark, "deliveryImages": delivery_images, "dispatchRemark": task.dispatch_remark, "items": item_details }) @router.post("/delivery-tasks/assign", response_model=dict) def assign_delivery_task( request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """分配配送任务""" request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user) if req_err is not None: return req_err from app.crud.delivery import delivery_task_crud from app.crud.order import order_crud order_id = request.get("orderId") deliver_id = request.get("deliverId") if not order_id: return error_response(400, "请选择订单") if not deliver_id: return error_response(400, "请选择配送员") # 验证配送员存在 deliver = user_crud.get(db, deliver_id) if not deliver: return error_response(404, "配送员不存在") if deliver.role != "deliver": return error_response(400, "该用户不是配送员") task = delivery_task_crud.assign_deliver( db, order_id=order_id, deliver_id=deliver_id, operator_id=current_user.get("user_id") ) if not task: return error_response(400, "分配失败,请检查订单状态") # 阶段D前收尾:分配配送优先维护 delivery_status order = order_crud.get(db, order_id) if order: order.delivery_status = "delivering" if order.order_status != "cancelled": order.order_status = "pending_confirm" order.status = derive_legacy_status_from_three( order_status=order.order_status, delivery_status=order.delivery_status, pay_status=order.pay_status, ) order.update_at = datetime.now() db.commit() return success_response({ "id": task.id, "status": task.status, "message": "分配成功" }) @router.post("/delivery-tasks/assign-batch", response_model=dict) def assign_delivery_task_batch( request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """批量分配配送任务""" request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user) if req_err is not None: return req_err from app.crud.delivery import delivery_task_crud from app.crud.order import order_crud assignments = request.get("assignments", []) if not assignments: return error_response(400, "请选择要分配的订单") results = [] for assignment in assignments: order_id = assignment.get("orderId") deliver_id = assignment.get("deliverId") if order_id and deliver_id: task = delivery_task_crud.assign_deliver( db, order_id=order_id, deliver_id=deliver_id, operator_id=current_user.get("user_id") ) if task: # 阶段D前收尾:分配配送优先维护 delivery_status order = order_crud.get(db, order_id) if order: order.delivery_status = "delivering" if order.order_status != "cancelled": order.order_status = "pending_confirm" order.status = derive_legacy_status_from_three( order_status=order.order_status, delivery_status=order.delivery_status, pay_status=order.pay_status, ) order.update_at = datetime.now() db.commit() results.append({"orderId": order_id, "success": True}) else: results.append({"orderId": order_id, "success": False, "reason": "分配失败"}) success_count = sum(1 for r in results if r.get("success")) return success_response({ "total": len(results), "success": success_count, "failed": len(results) - success_count, "results": results }) @router.get("/delivery-tasks/warnings", response_model=dict) def list_delivery_warnings( page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(10, ge=1, le=100, description="每页条数"), regionId: Optional[str] = Query(None, description="区域ID"), withinMinutes: int = Query(60, description="多少分钟内即将过期"), db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """获取配送时效预警列表""" from app.crud.delivery import delivery_task_crud tasks, total, warning_count = delivery_task_crud.get_warning_tasks( db, region_id=regionId, within_minutes=withinMinutes, skip=(page - 1) * pageSize, limit=pageSize ) region_map = {} deliver_map = {} result = [] for task in tasks: if task.region_id and task.region_id not in region_map: region = region_crud.get(db, task.region_id) region_map[task.region_id] = region.name if region else None if task.deliver_id and task.deliver_id not in deliver_map: deliver = user_crud.get(db, task.deliver_id) deliver_map[task.deliver_id] = deliver.real_name if deliver else None order_info = {} if task.order_id: order = order_crud.get(db, task.order_id) if order: order_info = {"orderNo": order.order_no} # 计算剩余时间和预警级别 remaining_minutes = 0 warning_level = "normal" is_overdue = False if task.effective_time: now = datetime.now() remaining = task.effective_time - now remaining_minutes = int(remaining.total_seconds() / 60) if remaining_minutes < 0: is_overdue = True warning_level = "overdue" elif remaining_minutes <= 15: warning_level = "urgent" elif remaining_minutes <= 30: warning_level = "warning" result.append({ "id": task.id, "taskNo": task.task_no, "orderId": task.order_id, "orderNo": order_info.get("orderNo"), "regionId": task.region_id, "regionName": region_map.get(task.region_id), "deliverId": task.deliver_id, "deliverName": deliver_map.get(task.deliver_id), "status": task.status, "expireTime": str(task.effective_time) if task.effective_time else None, "remainingMinutes": remaining_minutes, "isOverdue": is_overdue, "warningLevel": warning_level }) return success_response({ "list": result, "total": total, "warningCount": warning_count, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize }) @router.post("/delivery-tasks/{task_id}/cancel", response_model=dict) def cancel_delivery_task( task_id: str, request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """取消配送任务""" request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user) if req_err is not None: return req_err from app.crud.delivery import delivery_task_crud task = delivery_task_crud.get(db, task_id) if not task: return error_response(404, "配送任务不存在") reason = request.get("reason") task = delivery_task_crud.cancel_task( db, task_id=task_id, reason=reason, operator_id=current_user.get("user_id"), operator_real_name=current_user.get("real_name"), ) if not task: return error_response(400, "当前状态不支持取消") return success_response({ "id": task.id, "status": task.status, "message": "取消成功" }) @router.post("/delivery-tasks/{task_id}/reassign", response_model=dict) def reassign_delivery_task( task_id: str, request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """重新分配配送任务""" request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user) if req_err is not None: return req_err from app.crud.delivery import delivery_task_crud task = delivery_task_crud.get(db, task_id) if not task: return error_response(404, "配送任务不存在") new_deliver_id = request.get("deliverId") if not new_deliver_id: return error_response(400, "请选择新配送员") # 验证新配送员 new_deliver = user_crud.get(db, new_deliver_id) if not new_deliver: return error_response(404, "配送员不存在") if new_deliver.role != "deliver": return error_response(400, "该用户不是配送员") task = delivery_task_crud.reassign( db, task_id=task_id, new_deliver_id=new_deliver_id, operator_id=current_user.get("user_id"), operator_real_name=current_user.get("real_name"), ) if not task: return error_response(400, "当前状态不支持重新分配") return success_response({ "id": task.id, "deliverId": task.deliver_id, "message": "重新分配成功" }) @router.get("/delivery-tasks/pending", response_model=dict) def list_pending_delivery_tasks( page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(10, ge=1, le=100, description="每页条数"), regionId: Optional[str] = Query(None, description="区域ID"), db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """获取待分配的配送任务""" from app.crud.delivery import delivery_task_crud, delivery_task_item_crud tasks, total = delivery_task_crud.get_pending_list( db, region_id=regionId, skip=(page - 1) * pageSize, limit=pageSize ) region_map = {} result = [] for task in tasks: if task.region_id and task.region_id not in region_map: region = region_crud.get(db, task.region_id) region_map[task.region_id] = region.name if region else None order_info = {} if task.order_id: order = order_crud.get(db, task.order_id) if order: order_info = { "orderNo": order.order_no, "totalAmount": float(order.total_amount) if order.total_amount else 0, "contactPerson": order.contact_person, "contactPhone": order.contact_phone, "deliveryAddress": order.delivery_address } items = delivery_task_item_crud.get_by_task(db, task.id) result.append({ "id": task.id, "taskNo": task.task_no, "orderId": task.order_id, "orderNo": order_info.get("orderNo"), "regionId": task.region_id, "regionName": region_map.get(task.region_id), "status": task.status, "totalAmount": order_info.get("totalAmount"), "itemCount": len(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 })