diff --git a/backend/app/api/orders.py b/backend/app/api/orders.py index 2cda552..c0d7199 100644 --- a/backend/app/api/orders.py +++ b/backend/app/api/orders.py @@ -22,6 +22,7 @@ from backend.app.schemas.orders import ( ChangeOrderStatusRequest, ConfirmSupplierTextRequest, CreateOrderRequest, + ReviseOrderRequest, SupplierTextRequest, UpdateOrderRequest, UpdateOrderTrackingRequest, @@ -182,6 +183,29 @@ def update_order( return success_payload(order) +@router.post("/{order_id}/revise") +def revise_order( + order_id: int, + payload: ReviseOrderRequest, + order_service: OrderService = Depends(get_order_service), + session: Session = Depends(get_db_session), + current_user: dict = Depends(require_roles("manager", "admin")), + _permission_user: dict = Depends(require_permissions("order:update")), +) -> dict: + """管理员修订订单(修改商品后自动重算成本) + + 用途:管理员修改订单商品信息后,系统自动调用定价引擎重新计算成本, + 记录修改前后差异,生成审计日志。 + 请求参数:order_id(路径参数)+ ReviseOrderRequest(修改后商品列表和修订原因)。 + 返回值:修订结果,包含修改前后差异。 + 权限要求:经理或管理员,且需 order:update 权限。 + """ + result = order_service.revise_order(order_id, payload.model_dump(), session, current_user) + if not result: + raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) + return success_payload(result) + + @router.get("/{order_id}") def get_order( order_id: int, # 订单 ID(路径参数) diff --git a/backend/app/api/pricing.py b/backend/app/api/pricing.py index 78d6386..bf27f9a 100644 --- a/backend/app/api/pricing.py +++ b/backend/app/api/pricing.py @@ -484,6 +484,69 @@ def calculate_quotation( }) +# ====================================================================== +# AI 辅助定价 +# ====================================================================== + +@router.post("/quotation/ai-suggest") +def ai_suggest_pricing( + payload: dict, + session: Session = Depends(get_db_session), + _user: dict = Depends(require_roles("admin", "manager", "salesman")), +) -> dict: + """AI 辅助定价建议(规则缺失时的兜底方案) + + 当产品没有配置定价规则时,调用此接口获取 AI 建议的定价。 + AI 只做建议,最终需要人工确认后才能使用。 + + 请求参数:{ product_id, quantity, length_m?, width_m?, weight_g?, specification? } + 返回值:{ suggested_cost, ai_reason, ai_confidence, needs_manual_confirmation } + """ + from backend.app.models.business import Product + + product_id = payload.get("product_id") + if not product_id: + raise AppException(code=ErrorCode.PARAM_ERROR, message="product_id 不能为空", status_code=400) + + product = session.query(Product).filter(Product.id == product_id).first() + if not product: + raise AppException(code=ErrorCode.NOT_FOUND, message="产品不存在", status_code=404) + + # 基于产品基础信息给出建议 + cost_price = float(product.cost_price or 0) + quantity = float(payload.get("quantity", 1) or 1) + length_m = payload.get("length_m") + width_m = payload.get("width_m") + weight_g = payload.get("weight_g") + + suggested_cost = 0 + ai_reason = "" + + if cost_price > 0: + # 有基础成本价:按数量计算 + if length_m and width_m: + area = float(length_m) * float(width_m) + suggested_cost = round(area * cost_price * quantity, 2) + ai_reason = f"基于产品成本价 ¥{cost_price}/㎡,面积 {area}㎡ × {quantity}件" + elif weight_g: + suggested_cost = round(float(weight_g) / 1000 * cost_price * quantity, 2) + ai_reason = f"基于产品成本价 ¥{cost_price}/kg,重量 {weight_g}g × {quantity}件" + else: + suggested_cost = round(cost_price * quantity, 2) + ai_reason = f"基于产品成本价 ¥{cost_price} × {quantity}件" + else: + ai_reason = "产品未设置基础成本价,无法自动建议" + suggested_cost = 0 + + return { + "suggested_cost": suggested_cost, + "ai_reason": ai_reason, + "ai_confidence": 0.6 if cost_price > 0 else 0.1, + "needs_manual_confirmation": True, + "ai_assisted": True, + } + + # ====================================================================== # 序列化工具(将 ORM 模型转换为字典,便于返回 JSON 响应) # ====================================================================== diff --git a/backend/app/schemas/orders.py b/backend/app/schemas/orders.py index 0324429..283e940 100644 --- a/backend/app/schemas/orders.py +++ b/backend/app/schemas/orders.py @@ -32,6 +32,12 @@ class OrderListQuery(BaseModel): order_source: str | None = None """订单来源筛选""" + order_type: str | None = None + """订单类型筛选(industry/daily)""" + + need_invoice: int | None = None + """是否需要发票筛选(0=否,1=是)""" + start_time: str | None = None """查询起始时间(ISO 格式)""" @@ -264,7 +270,7 @@ class ApproveOrderRequest(BaseModel): """审批订单请求体。被 POST /api/orders/{id}/approve 路由使用。""" approve_result: str - """审批结果(如 approved / rejected)""" + """审批结果(pass=通过, reject=退回)""" approve_opinion: str | None = None """审批意见""" @@ -273,13 +279,7 @@ class ApproveOrderRequest(BaseModel): """下发工厂 ID(审批通过时可选,同时选择工厂和司机可跳过待工厂节点)""" driver_id: int | None = None - """司机 ID(审批通过时可选,同时选择工厂和司机可跳过待工厂节点)""" - - factory_id: int | None = None - """下发工厂 ID(非自发订单审批通过时可选传入,审批后直接绑定工厂)""" - - driver_id: int | None = None - """分配司机 ID(非自发订单审批通过时可选传入,审批后直接创建物流任务)""" + """分配司机 ID(审批通过时可选,同时选择工厂和司机可直接创建物流任务)""" class SupplierTextRequest(BaseModel): @@ -312,6 +312,31 @@ class ChangeOrderStatusRequest(BaseModel): """备注""" +class ReviseOrderRequest(BaseModel): + """管理员修订订单请求体。被 POST /api/orders/{id}/revise 路由使用。""" + + items: list[OrderItemPayload] = Field(min_length=1) + """修改后的订单商品明细列表(全量替换,至少一项)""" + + revision_reason: str | None = None + """修订原因""" + + contract_amount: float | None = None + """合同金额(可选,不传则保持原值)""" + + rebate_total: float = Field(default=0, ge=0) + """返利合计""" + + freight_total: float = Field(default=0, ge=0) + """运费合计""" + + tax_total: float = Field(default=0, ge=0) + """税额合计""" + + other_fee_total: float = Field(default=0, ge=0) + """其他费用合计""" + + class UpdateOrderTrackingRequest(BaseModel): """修改自发订单快递单号请求体。被 PUT /api/orders/{id}/tracking 路由使用。""" diff --git a/backend/app/services/order_service.py b/backend/app/services/order_service.py index 0d6f3d9..62a8aae 100644 --- a/backend/app/services/order_service.py +++ b/backend/app/services/order_service.py @@ -16,6 +16,7 @@ from sqlalchemy.orm import Session from backend.app.core.cache import cache_delete, cache_delete_pattern, cache_get, cache_set, make_cache_key from backend.app.core.error_codes import ErrorCode from backend.app.core.exceptions import AppException +from backend.app.repositories.audit_repository import AuditRepository from backend.app.repositories.customer_repository import CustomerRepository from backend.app.repositories.config_repository import ConfigRepository from backend.app.repositories.file_repository import FileRepository @@ -27,6 +28,7 @@ from backend.app.repositories.reminder_repository import ReminderRepository from backend.app.services.arrears_service import arrears_service from backend.app.services.audit_service import audit_service from backend.app.services.event_bus import event_bus +from backend.app.services.pricing_engine import pricing_engine class OrderService: @@ -53,6 +55,99 @@ class OrderService: self.supplier_repository = SupplierRepository() self.file_repository = FileRepository() self.reminder_repository = ReminderRepository() + self.audit_repository = AuditRepository() + + def _enrich_items_with_pricing(self, session: Session, items: list[dict]) -> list[dict]: + """使用定价引擎为订单项计算成本并填充快照字段。 + + 对每个商品项: + 1. 查找对应的定价规则 (ProductPricingRule) + 2. 调用定价引擎计算 + 3. 用引擎结果覆盖 cost_price 并填充快照字段 + 如果找不到规则或计算失败,保留原始 cost_price 不变。 + + Args: + session: 数据库会话 + items: 订单项列表(来自前端 payload) + + Returns: + 填充了定价快照字段的订单项列表 + """ + from backend.app.models.business import ProductPricingRule, Product + + enriched = [] + for item in items: + product_id = item.get("product_id") + if not product_id: + enriched.append(item) + continue + + # 查找定价规则 + rule = session.query(ProductPricingRule).filter( + ProductPricingRule.product_id == product_id, + ProductPricingRule.deleted == 0, + ProductPricingRule.status == 1, + ).first() + + if not rule: + enriched.append(item) + continue + + # 获取产品属性(厚度、克重等) + product = session.query(Product).filter(Product.id == product_id).first() + product_attrs = {} + if product: + if product.thickness: + product_attrs["thickness"] = product.thickness + if product.weight_gsm: + product_attrs["weight_gsm"] = product.weight_gsm + if product.default_width_m: + product_attrs["default_width_m"] = float(product.default_width_m) + + # 构建用户输入:从 item 中提取定价相关字段 + user_inputs = {} + if item.get("length_m"): + user_inputs["length"] = item["length_m"] + user_inputs["length_unit"] = "m" + if item.get("width_m"): + user_inputs["width"] = item["width_m"] + user_inputs["width_unit"] = "m" + if item.get("quantity"): + user_inputs["quantity"] = item["quantity"] + if item.get("price_tier"): + user_inputs["price_tier"] = item["price_tier"] + # 从 surcharge_detail 中恢复附加费选择 + surcharge_detail = item.get("surcharge_detail") + if surcharge_detail: + try: + surcharges = json.loads(surcharge_detail) if isinstance(surcharge_detail, str) else surcharge_detail + if isinstance(surcharges, list): + for s in surcharges: + key = s.get("key") + if key: + user_inputs[f"surcharge_{key}"] = True + except (json.JSONDecodeError, TypeError): + pass + + try: + result = pricing_engine.calculate_for_order_item(rule, user_inputs, product_attrs) + # 用引擎结果覆盖成本价 + item_copy = dict(item) + item_copy["cost_price"] = result["cost_price"] + # 填充快照字段 + snapshot = result.get("item_snapshot", {}) + if snapshot.get("pricing_type"): + item_copy["pricing_type"] = snapshot["pricing_type"] + if result.get("area_sqm"): + item_copy["area_sqm"] = result["area_sqm"] + if snapshot.get("surcharge_detail"): + item_copy["surcharge_detail"] = snapshot["surcharge_detail"] + enriched.append(item_copy) + except Exception: + # 计价失败时保留原始数据 + enriched.append(item) + + return enriched def list_orders(self, session: Session | None = None, filters: dict | None = None, current_user: dict | None = None) -> dict: """查询订单列表。 @@ -292,10 +387,11 @@ class OrderService: *[self._build_attachment_row(item) for item in order_attachments], *[self._build_attachment_row(item) for item in task_attachments], ], - "calculation_detail": self._build_calculation_detail(order, items), + "calculation_detail": self._build_calculation_detail(order, items, session), "profit_alert_threshold": float(self._get_config_value(session, "profit_alert_threshold", "0")), "customer_demand": order.customer_demand, "remark": order.remark, + "revision_info": self._get_latest_revision(session, order.id), } # 写缓存(缓存完整结果,角色过滤在读取后执行) cache_set(cache_key, result, ttl=60) @@ -361,6 +457,10 @@ class OrderService: items = payload["items"] if not items: raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400) + + # 调用定价引擎为每个订单项计算成本并生成快照 + items = self._enrich_items_with_pricing(session, items) + sale_total = sum(item["quantity"] * item["sale_price"] for item in items) cost_total = sum(item["quantity"] * item["cost_price"] for item in items) # 使用合同金额作为收入计算利润,如果没有合同金额则使用系统计算的销售金额 @@ -447,6 +547,170 @@ class OrderService: raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库连接不可用", status_code=500) + def revise_order( + self, + order_id: int, + payload: dict, + session: Session | None = None, + current_user: dict | None = None, + ) -> dict | None: + """管理员修订订单(修改商品后自动重算)。 + + 与 update_order 不同,此方法: + 1. 记录修改前的快照 + 2. 调用定价引擎重新计算所有商品成本 + 3. 生成修订版本记录 + 4. 返回修改前后差异 + + 仅 manager/admin 角色可调用。 + + Args: + order_id: 订单 ID + payload: 包含 items(修改后商品列表)和 revision_reason(修改原因) + session: 数据库会话 + current_user: 当前登录用户信息 + + Returns: + 包含修订结果和前后差异的字典 + """ + if session is not None: + try: + order = self.order_repository.get_order(session, order_id) + if order is None: + return None + # 管理员可以修订任何非终态订单 + terminal_statuses = {"canceled", "settled"} + if order.order_status in terminal_statuses: + raise AppException( + code=ErrorCode.PARAM_ERROR, + message="已取消或已结算的订单不允许修订", + status_code=400, + ) + + items = payload.get("items", []) + if not items: + raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400) + + # 记录修改前的快照 + old_items = self.order_repository.list_order_items(session, order_id) + before_snapshot = { + "order": { + "sale_price_total": float(order.sale_price_total or 0), + "cost_price_total": float(order.cost_price_total or 0), + "profit_total": float(order.profit_total or 0), + "profit_rate": float(order.profit_rate or 0), + }, + "items": [ + { + "product_name": item.product_name, + "specification": item.specification, + "quantity": float(item.quantity or 0), + "cost_price": float(item.cost_price or 0), + "sale_price": float(item.sale_price or 0), + } + for item in old_items + ], + } + + # 调用定价引擎重新计算 + items = self._enrich_items_with_pricing(session, items) + + sale_total = sum(item["quantity"] * item["sale_price"] for item in items) + cost_total = sum(item["quantity"] * item["cost_price"] for item in items) + income_amount = payload.get("contract_amount") or order.contract_amount or sale_total + rebate_total = payload.get("rebate_total", float(order.rebate_total or 0)) + freight_total = payload.get("freight_total", float(order.freight_total or 0)) + tax_total = payload.get("tax_total", float(order.tax_total or 0)) + other_fee_total = payload.get("other_fee_total", float(order.other_fee_total or 0)) + profit_total = income_amount - cost_total - rebate_total - freight_total - tax_total - other_fee_total + profit_rate = round((profit_total / income_amount) * 100, 2) if income_amount else 0 + + self.order_repository.update_order_with_items( + session, + order, + { + "sale_price_total": sale_total, + "cost_price_total": cost_total, + "rebate_total": rebate_total, + "freight_total": freight_total, + "tax_total": tax_total, + "other_fee_total": other_fee_total, + "profit_total": profit_total, + "profit_rate": profit_rate, + }, + items, + ) + + # 记录修改后的快照 + after_snapshot = { + "order": { + "sale_price_total": round(sale_total, 2), + "cost_price_total": round(cost_total, 2), + "profit_total": round(profit_total, 2), + "profit_rate": profit_rate, + }, + "items": [ + { + "product_name": item.get("product_name"), + "specification": item.get("specification"), + "quantity": item.get("quantity"), + "cost_price": item.get("cost_price"), + "sale_price": item.get("sale_price"), + } + for item in items + ], + } + + # 写审计日志 + audit_service.write_log( + session, + { + "operate_type": "order_revise", + "biz_type": "sales_order", + "biz_id": order.id, + "before_value": before_snapshot, + "after_value": after_snapshot, + "remark": f"修订订单 {order.order_no}:{payload.get('revision_reason', '管理员修改')}", + }, + ) + + # 写审批日志(使修订记录出现在审批时间线中) + self.order_approve_log_repository.create_log( + session, + { + "order_id": order.id, + "approve_type": "revise", + "approve_result": "revised", + "before_status": order.order_status, + "after_status": order.order_status, + "approve_opinion": payload.get("revision_reason", "管理员修订"), + "operator_id": current_user.get("user_id") if current_user else None, + }, + ) + + session.commit() + self._invalidate_order_cache(order.id) + + return { + "order_id": order.id, + "order_no": order.order_no, + "order_status": order.order_status, + "sale_price_total": round(sale_total, 2), + "cost_price_total": round(cost_total, 2), + "profit_total": round(profit_total, 2), + "profit_rate": profit_rate, + "revision_reason": payload.get("revision_reason", ""), + "before": before_snapshot, + "after": after_snapshot, + } + except AppException: + session.rollback() + raise + except SQLAlchemyError as exc: + session.rollback() + raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc + raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库连接不可用", status_code=500) + def submit_order( self, order_id: int, @@ -656,35 +920,18 @@ class OrderService: else: target_status = "rejected" # 如果有工厂ID,设置到订单上 - if payload.get("factory_id") and target_status in ("pending_factory",): + if payload.get("factory_id") and target_status in ("pending_factory", "pending_driver"): order.factory_id = payload["factory_id"] - # 同时选择了工厂和司机:在同一个事务内创建物流任务,直接进入待司机接单 - if payload.get("factory_id") and payload.get("driver_id") and target_status == "pending_factory": - task = self.logistics_repository.create_task( - session, - { - "task_no": f"LT{datetime.now().strftime('%Y%m%d%H%M%S')}", - "order_id": order.id, - "driver_id": payload["driver_id"], - "factory_id": payload["factory_id"], - "pickup_address": payload.get("pickup_address", ""), - "delivery_address": payload.get("delivery_address", ""), - "pickup_content": payload.get("pickup_content", ""), - "quantity": payload.get("quantity", 1), - "status": "pending", - "created_by": current_user.get("user_id") if current_user else None, - }, - ) - target_status = "pending_driver" self.order_repository.update_order_status(session, order, target_status) - # 有工厂+司机时,在同一事务内创建物流任务 + # 审批通过且有工厂+司机时,创建物流任务 if payload["approve_result"] == "pass" and target_status == "pending_driver" and payload.get("driver_id"): # 获取工厂地址作为取货地址 pickup_address = "" - factory = self.supplier_repository.get_supplier(session, payload["factory_id"]) - if factory and factory.address: - pickup_address = factory.address + if payload.get("factory_id"): + factory = self.supplier_repository.get_supplier(session, payload["factory_id"]) + if factory and factory.address: + pickup_address = factory.address task = self.logistics_repository.create_task( session, @@ -692,7 +939,7 @@ class OrderService: "task_no": f"LT{datetime.now().strftime('%Y%m%d%H%M%S')}", "order_id": order.id, "driver_id": payload["driver_id"], - "factory_id": payload["factory_id"], + "factory_id": payload.get("factory_id"), "pickup_address": pickup_address, "delivery_address": "", "pickup_content": "", @@ -1241,6 +1488,9 @@ class OrderService: if not items: raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400) + # 调用定价引擎为每个订单项计算成本并生成快照 + items = self._enrich_items_with_pricing(session, items) + sale_total = sum(item["quantity"] * item["sale_price"] for item in items) cost_total = sum(item["quantity"] * item["cost_price"] for item in items) # 使用合同金额作为收入计算利润,如果没有合同金额则使用系统计算的销售金额 @@ -1687,7 +1937,7 @@ class OrderService: if approve_result == "refuse" and not (payload.get("approve_opinion") or "").strip(): raise AppException(code=ErrorCode.PARAM_ERROR, message="取消审批驳回时必须填写意见", status_code=400) - def _build_calculation_detail(self, order, items) -> dict: + def _build_calculation_detail(self, order, items, session=None) -> dict: """构建订单利润计算明细。 包含公式说明、各费用汇总和每条明细的金额拆分,支持报价引擎快照数据。 @@ -1695,6 +1945,7 @@ class OrderService: Args: order: 订单 ORM 对象 items: 订单明细 ORM 对象列表 + session: 数据库会话(可选,用于查询定价规则公式) Returns: 包含计算公式、汇总金额和明细的字典 @@ -1739,6 +1990,21 @@ class OrderService: detail_row["surcharge_items"] = [] detail_row["supplier_model"] = getattr(item, "supplier_model", None) detail_row["price_tier"] = getattr(item, "price_tier", None) + # 查询定价规则公式 + if session and item.product_id: + try: + from backend.app.models.business import ProductPricingRule + rule = session.query(ProductPricingRule).filter( + ProductPricingRule.product_id == item.product_id, + ProductPricingRule.deleted == 0, + ).first() + if rule: + detail_row["formula_expr"] = getattr(rule, "formula_expr", None) + detail_row["formula_note"] = getattr(rule, "formula_note", None) + if not detail_row.get("pricing_type"): + detail_row["pricing_type"] = getattr(rule, "pricing_type", None) + except Exception: + pass item_details.append(detail_row) # 税计算说明 @@ -1838,6 +2104,39 @@ class OrderService: "created_at": task.created_at.strftime("%Y-%m-%d %H:%M:%S") if task.created_at else "", } + def _get_latest_revision(self, session: Session, order_id: int) -> dict | None: + """获取订单最近一次修订记录。 + + 从审计日志中查找 operate_type=order_revise 的最新记录, + 返回修订前后的差异快照,供审批页展示。 + + Args: + session: 数据库会话 + order_id: 订单 ID + + Returns: + 修订信息字典,无修订记录时返回 None + """ + try: + logs = self.audit_repository.list_logs(session, { + "biz_type": "sales_order", + "biz_id": order_id, + "operate_type": "order_revise", + }) + if not logs: + return None + latest = logs[0] # 按 id 倒序,第一个是最新的 + before = json.loads(latest.before_value) if latest.before_value else None + after = json.loads(latest.after_value) if latest.after_value else None + return { + "revision_time": latest.operate_time.strftime("%Y-%m-%d %H:%M:%S") if latest.operate_time else "", + "revision_remark": latest.remark or "", + "before": before, + "after": after, + } + except Exception: + return None + def _build_attachment_row(self, attachment) -> dict: """构建附件行数据字典。 diff --git a/backend/app/services/pricing_engine.py b/backend/app/services/pricing_engine.py index 10128a9..e774b44 100644 --- a/backend/app/services/pricing_engine.py +++ b/backend/app/services/pricing_engine.py @@ -30,24 +30,23 @@ class PricingEngine: product_attrs: 产品属性 {"thickness": 0.08} 用于阈值判断 Returns: - { - "base_cost", "surcharge_items", "total_surcharge", "cost_price", - "formula_detail", "formula_detail_steps" - } + 结构化计价结果字典,包含 base_cost、surcharge_items、total_surcharge、 + cost_price、formula_detail、tax 字段,以及 rule_snapshot 用于快照存储。 被调用路由: pricing.py - POST /pricing/calculate """ product_attrs = product_attrs or {} - # 1. 单位换算 → 统一为米 + # 1. 单位换算 normalized = self._normalize_inputs(rule, user_inputs) area_sqm = normalized.get("length_m", 0) * normalized.get("width_m", 0) # 2. 构建公式上下文并求值 + formula_constants = self._safe_load_json(rule.formula_constants) or {} context = { "input": normalized, "rule": {"base_unit_price": float(rule.base_unit_price)}, - "const": self._safe_load_json(rule.formula_constants) or {}, + "const": formula_constants, "product": product_attrs, } base_cost = self._evaluate_formula(rule.formula_expr or "0", context) @@ -67,12 +66,10 @@ class PricingEngine: tax_inclusive = int(getattr(rule, "tax_inclusive", 0) or 0) if tax_rate > 0: if tax_inclusive: - # 含税定价:cost_price 已含税,反算不含税价 price_ex_tax = round(cost_price / (1 + tax_rate / 100), 2) tax_amount = round(cost_price - price_ex_tax, 2) price_in_tax = cost_price else: - # 不含税定价:cost_price 为不含税价,正算含税价 price_ex_tax = cost_price tax_amount = round(cost_price * tax_rate / 100, 2) price_in_tax = round(cost_price + tax_amount, 2) @@ -81,16 +78,70 @@ class PricingEngine: tax_amount = 0 price_in_tax = cost_price + # 6. 构建变量快照(用于审计和回显) + formula_variables = {} + for prefix, values in context.items(): + if isinstance(values, dict): + for k, v in values.items(): + formula_variables[f"{prefix}.{k}"] = v + + # 7. 构建定价规则快照 + rule_snapshot = { + "pricing_rule_id": getattr(rule, "id", None), + "pricing_type": getattr(rule, "pricing_type", None), + "pricing_unit": getattr(rule, "pricing_unit", None), + "base_unit_price": float(rule.base_unit_price or 0), + "formula_expr": getattr(rule, "formula_expr", None), + "formula_note": getattr(rule, "formula_note", None), + } + return { "base_cost": round(base_cost, 2), "surcharge_items": surcharges, "total_surcharge": total_surcharge, "cost_price": cost_price, "formula_detail": formula_detail, + "formula_variables": formula_variables, "tax_rate": tax_rate, + "tax_inclusive": tax_inclusive, "price_ex_tax": price_ex_tax, "tax_amount": tax_amount, "price_in_tax": price_in_tax, + "area_sqm": round(area_sqm, 6), + "rule_snapshot": rule_snapshot, + # AI 辅助标记(当前由规则引擎计算,ai_assisted=False) + # 当规则缺失时由 AI 填充,需人工确认 + "ai_assisted": False, + "ai_reason": None, + "ai_confidence": None, + "needs_manual_confirmation": False, + } + + def calculate_for_order_item(self, rule, user_inputs: dict, product_attrs: dict | None = None) -> dict: + """为订单项执行计价并返回可直接存入快照的结构化结果。 + + 在 calculate() 基础上,额外返回适合存入 SalesOrderItem 的字段映射。 + + Args: + rule: ProductPricingRule ORM 对象 + user_inputs: 前端传入的输入 + product_attrs: 产品属性 + + Returns: + 包含 item_snapshot 字段的计价结果,可直接写入订单项 + """ + result = self.calculate(rule, user_inputs, product_attrs) + snapshot = result["rule_snapshot"] + return { + **result, + "item_snapshot": { + "pricing_type": snapshot.get("pricing_type"), + "pricing_unit": snapshot.get("pricing_unit"), + "area_sqm": result["area_sqm"], + "surcharge_detail": json.dumps(result["surcharge_items"], ensure_ascii=False) if result["surcharge_items"] else None, + "price_tier": user_inputs.get("price_tier"), + "cost_price": result["cost_price"], + }, } def get_available_surcharge_options(self, rule) -> list[dict]: @@ -118,20 +169,28 @@ class PricingEngine: _UNIT_TO_M = {"m": 1.0, "cm": 0.01, "mm": 0.001} + # 重量单位 → 千克 + _UNIT_TO_KG = {"kg": 1.0, "g": 0.001, "t": 1000.0} + + # 面积单位 → 平方米 + _UNIT_TO_SQM = {"sqm": 1.0, "sqcm": 0.0001, "sqmm": 0.000001} + def _normalize_inputs(self, rule, user_inputs: dict) -> dict: - """将前端输入转换为统一的米制数值。 + """将前端输入转换为统一的标准单位数值。 根据 pricing_inputs 声明解析字段,自动读取 _unit 后缀进行单位换算。 + - 长度类字段统一转为米(_m 后缀) + - 重量类字段统一转为千克(_kg 后缀) + - 面积类字段统一转为平方米(_sqm 后缀) Args: rule: 定价规则 ORM 对象 user_inputs: 前端传入的原始输入字典 Returns: - 标准化后的输入字典,包含原始值和 _m 后缀的米制值 + 标准化后的输入字典,包含原始值和标准化后缀值 """ normalized = {} - # 解析 pricing_inputs 声明 inputs_decl = self._safe_load_json(rule.pricing_inputs) or [] for field in inputs_decl: @@ -146,11 +205,20 @@ class PricingEngine: normalized[key] = val continue - # 带单位的数值字段:读取对应 _unit 后缀 unit_key = f"{key}_unit" unit = user_inputs.get(unit_key, field.get("default_unit", "m")) - multiplier = self._UNIT_TO_M.get(unit, 1.0) - normalized[f"{key}_m"] = round(val * multiplier, 6) + field_type = field.get("unit_type", "length") + + if field_type == "weight": + multiplier = self._UNIT_TO_KG.get(unit, 1.0) + normalized[f"{key}_kg"] = round(val * multiplier, 6) + elif field_type == "area": + multiplier = self._UNIT_TO_SQM.get(unit, 1.0) + normalized[f"{key}_sqm"] = round(val * multiplier, 6) + else: + multiplier = self._UNIT_TO_M.get(unit, 1.0) + normalized[f"{key}_m"] = round(val * multiplier, 6) + normalized[key] = val return normalized @@ -165,6 +233,8 @@ class PricingEngine: 支持 $input.xxx、$rule.xxx、$const.xxx、$product.xxx 变量引用, 以及 ${condition} ? a : b 三元条件表达式。 + 使用正则替换(按变量名长度降序)避免短变量名误匹配长变量名前缀。 + Args: expr: 公式表达式字符串 context: 变量上下文字典 @@ -174,12 +244,20 @@ class PricingEngine: """ resolved = expr - # 替换 $input.xxx / $rule.xxx / $const.xxx / $product.xxx + # 收集所有 (完整变量名, 替换值),按变量名长度降序排列防止前缀误匹配 + replacements = [] for prefix, values in context.items(): if not isinstance(values, dict): continue for k, v in values.items(): - resolved = resolved.replace(f"${prefix}.{k}", self._to_str(v)) + var_name = f"${prefix}.{k}" + replacements.append((var_name, self._to_str(v))) + replacements.sort(key=lambda x: len(x[0]), reverse=True) + + for var_name, replacement in replacements: + # 用 re.escape 转义变量名中的特殊字符(如 $ 和 .) + pattern = re.escape(var_name) + resolved = re.sub(pattern, replacement, resolved) # 解析 ${condition} ? a : b resolved = self._resolve_conditionals(resolved) @@ -285,7 +363,11 @@ class PricingEngine: def _build_formula_detail(self, rule, normalized: dict, base_cost: float, surcharges: list) -> str: """构建人类可读的计算过程文本。 - 输出格式示例:2m x 1.2m x ¥50/㎡ = ¥120.00 -> + 加急费 ¥20.00 = ¥140.00 + 根据定价类型自动生成对应的计算说明: + - area: 2m x 1.2m x ¥50/㎡ = ¥120.00 + - kg: 5kg x ¥8/kg = ¥40.00 + - unit/件: 10件 x ¥25/件 = ¥250.00 + - linear_m: 20m x ¥15/m = ¥300.00 Args: rule: 定价规则 ORM 对象 @@ -296,10 +378,30 @@ class PricingEngine: Returns: 公式说明字符串 """ + pricing_type = getattr(rule, "pricing_type", "area") or "area" + unit_price = float(rule.base_unit_price or 0) parts = [] - length = normalized.get("length_m", 0) - width = normalized.get("width_m", 0) - parts.append(f"{length}m × {width}m × ¥{rule.base_unit_price}/㎡ = ¥{base_cost:.2f}") + + if pricing_type == "area": + length = normalized.get("length_m", 0) + width = normalized.get("width_m", 0) + parts.append(f"{length}m × {width}m × ¥{unit_price}/㎡ = ¥{base_cost:.2f}") + elif pricing_type in ("kg", "weight_g"): + weight = normalized.get("weight_kg", normalized.get("weight_kg", 0)) + if pricing_type == "weight_g": + weight_g = weight * 1000 if weight else normalized.get("weight", 0) + parts.append(f"{weight_g}g × ¥{unit_price}/g = ¥{base_cost:.2f}") + else: + parts.append(f"{weight}kg × ¥{unit_price}/kg = ¥{base_cost:.2f}") + elif pricing_type == "linear_m": + length = normalized.get("length_m", normalized.get("length", 0)) + parts.append(f"{length}m × ¥{unit_price}/m = ¥{base_cost:.2f}") + elif pricing_type in ("unit", "件"): + qty = normalized.get("quantity", normalized.get("qty", 0)) + parts.append(f"{qty}件 × ¥{unit_price}/件 = ¥{base_cost:.2f}") + else: + # 通用:显示公式结果 + parts.append(f"计算结果 = ¥{base_cost:.2f}") for s in surcharges: parts.append(f"+ {s['name']} ¥{s['amount']:.2f}") @@ -307,7 +409,6 @@ class PricingEngine: total = base_cost + sum(s["amount"] for s in surcharges) parts.append(f"= ¥{total:.2f}") - # 税说明 tax_rate = float(getattr(rule, "tax_rate", 0) or 0) if tax_rate > 0: tax_inclusive = int(getattr(rule, "tax_inclusive", 0) or 0) diff --git a/frontend/mini-app/pages/manager/approve-detail/approve-detail.js b/frontend/mini-app/pages/manager/approve-detail/approve-detail.js index 1748497..85a4484 100644 --- a/frontend/mini-app/pages/manager/approve-detail/approve-detail.js +++ b/frontend/mini-app/pages/manager/approve-detail/approve-detail.js @@ -135,6 +135,8 @@ Page({ for (var i = 0; i < rawItems.length; i++) { var ci = rawItems[i]; if (ci.product_name) { + // 计价方式中文映射 + var pricingTypeMap = { area: "按面积", kg: "按重量", unit: "按件", 件: "按件", linear_m: "按米", weight_g: "按克重" }; products.push({ name: ci.product_name, spec: ci.specification || "", @@ -143,6 +145,10 @@ Page({ unit: ci.unit || "", costPrice: fmt(ci.cost_price), costAmount: fmt(Number(ci.quantity || 0) * Number(ci.cost_price || 0)), + pricingType: pricingTypeMap[ci.pricing_type] || "", + areaSqm: ci.area_sqm ? fmt(ci.area_sqm) + "㎡" : "", + supplierModel: ci.supplier_model || "", + priceTier: ci.price_tier || "", }); } } @@ -318,23 +324,7 @@ Page({ url: targetUrl, method: "POST", data: payload, }).then(function () { - // approve pass + driver_id: create logistics task (mirrors web flow) - if (payload.approve_result === "pass" && payload.driver_id) { - var orderData = that.data.orderInfo; - return app.request({ - url: "/api/logistics/tasks", - method: "POST", - data: { - order_id: orderData.order_id, - driver_id: Number(payload.driver_id), - pickup_address: orderData.factory_name || "", - delivery_address: orderData.customer_address || "", - pickup_content: orderData.customer_name ? orderData.customer_name + " 订单货物" : "", - quantity: 1, - factory_id: payload.factory_id, - }, - }); - } + // 物流任务由后端审批接口统一创建,前端不再重复创建 }).then(function () { if (payload.approve_result === "pass") { // 从原始数据直接构建采购信息,避免 this.data 时序问题 diff --git a/frontend/mini-app/pages/manager/approve-detail/approve-detail.wxml b/frontend/mini-app/pages/manager/approve-detail/approve-detail.wxml index 8270beb..2966cfb 100644 --- a/frontend/mini-app/pages/manager/approve-detail/approve-detail.wxml +++ b/frontend/mini-app/pages/manager/approve-detail/approve-detail.wxml @@ -88,6 +88,9 @@ {{item.spec}} 需求: {{item.demandSpec}} {{item.qty}} {{item.unit}} + 计价: {{item.pricingType}} + 面积: {{item.areaSqm}} + 型号: {{item.supplierModel}} 单价: ¥{{item.costPrice}} @@ -120,12 +123,12 @@ 审批记录 - + {{item.approve_time}} - {{item.approve_type === 'order' ? '订单审批' : '取消审批'}} - {{item.approve_result === 'pass' ? '通过' : '驳回'}} + {{item.approve_type === 'order' ? '订单审批' : (item.approve_type === 'revise' ? '订单修订' : '取消审批')}} + {{item.approve_result === 'pass' ? '通过' : (item.approve_result === 'revised' ? '已修订' : '驳回')}} {{item.approve_opinion}} diff --git a/frontend/web-admin/src/mockApi.js b/frontend/web-admin/src/mockApi.js index c68a0ce..31ee3cf 100644 --- a/frontend/web-admin/src/mockApi.js +++ b/frontend/web-admin/src/mockApi.js @@ -208,6 +208,7 @@ function mapApproveType(type) { const typeMap = { order: "订单审批", cancel_order: "取消审批", + revise: "订单修订", }; return typeMap[type] || type || "-"; } @@ -222,6 +223,7 @@ function mapApproveResult(result) { pass: "通过", reject: "驳回", refuse: "驳回", + revised: "已修订", }; return resultMap[result] || result || "-"; } @@ -1140,6 +1142,19 @@ export async function updateOrder(orderId, payload) { }); } +/** + * 管理员修订订单(修改商品后自动重算成本) + * @param {number} orderId - 订单 ID + * @param {Object} payload - { items: [...], revision_reason?: string, ... } + * @returns {Promise} + */ +export async function reviseOrder(orderId, payload) { + return request(`/api/orders/${orderId}/revise`, { + method: "POST", + body: JSON.stringify(payload), + }); +} + /** * 修改自发订单快递单号 * @param {number} orderId - 订单 ID @@ -1434,6 +1449,7 @@ export async function fetchApprovalDetail(orderId) { approveOpinion: item.approve_opinion || "-", })), calculationDetail: data.calculation_detail || null, + revisionInfo: data.revision_info || null, }; } diff --git a/frontend/web-admin/src/views/ApprovalDetailPage.vue b/frontend/web-admin/src/views/ApprovalDetailPage.vue index af8d20b..ef9bdcb 100644 --- a/frontend/web-admin/src/views/ApprovalDetailPage.vue +++ b/frontend/web-admin/src/views/ApprovalDetailPage.vue @@ -39,20 +39,41 @@ 产品名称 规格 数量 + 计价方式 成本单价 + 成本小计 - + {{ item.productName }} {{ item.specification || '-' }} {{ item.quantity }}{{ item.unit }} + {{ getPricingTypeLabel(item, idx) }} {{ item.costPrice }} + {{ getItemCostTotal(item) }}
暂无产品明细
+ +
+

计算明细

+
+

{{ detail.product_name }}

+

计价方式:{{ getDetailPricingTypeLabel(detail.pricing_type) }}

+

公式说明:{{ detail.formula_note }}

+

公式:{{ detail.formula_expr }}

+

面积:{{ detail.area_sqm }}㎡

+

+ 附加费: + {{ s.name }} ¥{{ s.amount }}{{ si < detail.surcharge_items.length - 1 ? ',' : '' }} +

+

供应商型号:{{ detail.supplier_model }}

+

价格档位:{{ detail.price_tier }}

+
+
@@ -89,6 +110,32 @@ + +
+

修订记录

+

{{ revisionInfo.revision_time }} — {{ revisionInfo.revision_remark }}

+
+
+ 成本总额 + ¥{{ revisionInfo.before.order?.cost_price_total }} + + ¥{{ revisionInfo.after.order?.cost_price_total }} +
+
+ 利润总额 + ¥{{ revisionInfo.before.order?.profit_total }} + + ¥{{ revisionInfo.after.order?.profit_total }} +
+
+ 利润率 + {{ revisionInfo.before.order?.profit_rate }}% + + {{ revisionInfo.after.order?.profit_rate }}% +
+
+
+

审批操作

{{ actionHint }}

@@ -138,6 +185,7 @@ const loading = ref(true); const orderInfo = ref(null); const items = ref([]); const calcDetail = ref(null); +const revisionInfo = ref(null); const approveOpinion = ref(""); const actionLoading = ref(""); const isModal = computed(() => route.query.modal === "1"); @@ -173,6 +221,7 @@ async function loadDetail() { orderInfo.value = data.orderInfo; items.value = data.items || []; calcDetail.value = data.calculationDetail || null; + revisionInfo.value = data.revisionInfo || null; } function handleClose() { @@ -194,6 +243,34 @@ function buildCopyText() { return lines.join("\n"); } +const PRICING_TYPE_LABELS = { + area: "按面积", + kg: "按重量", + weight_g: "按克重", + unit: "按件", + 件: "按件", + linear_m: "按长度", +}; + +function getPricingTypeLabel(item, idx) { + // 优先从 calcDetail.item_details 中获取 + if (calcDetail.value && calcDetail.value.item_details && calcDetail.value.item_details[idx]) { + const pt = calcDetail.value.item_details[idx].pricing_type; + if (pt) return PRICING_TYPE_LABELS[pt] || pt; + } + return '-'; +} + +function getDetailPricingTypeLabel(pricingType) { + return PRICING_TYPE_LABELS[pricingType] || pricingType; +} + +function getItemCostTotal(item) { + const qty = Number(item.quantity) || 0; + const cost = Number(item.costPrice) || 0; + return (qty * cost).toFixed(2); +} + async function handleApprove(result) { if (!orderInfo.value) return; actionLoading.value = result; @@ -203,7 +280,7 @@ async function handleApprove(result) { approve_opinion: approveOpinion.value || (result === "pass" ? "审批通过" : "审批驳回"), }; - if (orderInfo.value.orderStatus === "cancel_pending") { + if (orderInfo.value.orderStatus === "cancel_pending" || orderInfo.value.orderStatus === "cancel_fulfillment_pending") { await cancelApproveOrder(orderInfo.value.orderId, payload); toast.success(result === "pass" ? "取消审批已通过" : "已驳回取消申请"); } else { @@ -300,6 +377,24 @@ td { color: #374151; } .calc-result { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e5e7eb; } .tax-note { color: #9ca3af; font-size: 12px; margin-left: 16px; } +/* 修订记录 */ +.revision-card { background: #fef3c7; border-color: #f59e0b; } +.revision-time { margin: 0 0 12px; color: #92400e; font-size: 13px; } +.revision-diff { display: flex; flex-direction: column; gap: 8px; } +.diff-row { display: flex; align-items: center; gap: 8px; font-size: 13px; } +.diff-label { color: #6b7280; min-width: 60px; } +.diff-before { color: #dc2626; text-decoration: line-through; } +.diff-arrow { color: #9ca3af; } +.diff-after { color: #16a34a; font-weight: 500; } + +/* 单项计算明细 */ +.item-calc-details { margin-top: 16px; padding-top: 16px; border-top: 1px solid #e5e7eb; } +.item-calc-details h4 { margin: 0 0 12px; font-size: 14px; color: #374151; } +.item-calc-row { padding: 8px 12px; background: #f9fafb; border-radius: 8px; margin-bottom: 8px; } +.item-calc-row:last-child { margin-bottom: 0; } +.item-calc-name { margin: 0 0 4px; font-weight: 500; color: #374151; font-size: 13px; } +.item-calc-row p { margin: 2px 0; color: #6b7280; font-size: 12px; } + /* 审批操作 */ .action-card { background: #f9fafb; } .action-hint { margin: 0 0 16px; color: #6b7280; font-size: 13px; } diff --git a/frontend/web-admin/src/views/OrdersPage.vue b/frontend/web-admin/src/views/OrdersPage.vue index d6dfcf8..6cfc4ed 100644 --- a/frontend/web-admin/src/views/OrdersPage.vue +++ b/frontend/web-admin/src/views/OrdersPage.vue @@ -163,7 +163,7 @@ {{ detail.order.statusText }} 自发 -
+
@@ -297,6 +297,10 @@ {{ detail.order.remark }}
+
+ + +
@@ -429,9 +433,9 @@
{{ log.approve_time }} - {{ log.approve_type === 'order' ? '订单审批' : '取消审批' }} - {{ - log.approve_result === 'pass' ? '通过' : '驳回' + {{ log.approve_type === 'order' ? '订单审批' : (log.approve_type === 'revise' ? '订单修订' : '取消审批') }} + {{ + log.approve_result === 'pass' ? '通过' : (log.approve_result === 'revised' ? '已修订' : '驳回') }}

{{ log.approve_opinion }}

@@ -900,6 +904,7 @@ import { cancelOrderById, fetchOrderDetailAdmin, updateOrder, + reviseOrder, submitOrderToReview, changeOrderStatus, settleOrder, @@ -1070,6 +1075,7 @@ const editForm = reactive({ commission_amount: 0, payment_method: '', remark: '', + revision_reason: '', }); const editItems = ref([]); const productOptions = ref([]); @@ -1107,7 +1113,9 @@ function handleProductChange(item, event) { } function canEditOrder(status) { - return ['draft', 'rejected', 'pending_approve'].includes(status); + // 管理员可修订:草稿/已退回用 updateOrder,其他非终态用 reviseOrder + const terminalStatuses = ['canceled', 'settled']; + return !terminalStatuses.includes(status); } function enterEditMode() { @@ -1128,6 +1136,7 @@ function enterEditMode() { editForm.payment_method = detail.order.paymentMethod === '-' ? '' : detail.order.paymentMethod; editForm.remark = detail.order.remark === '-' ? '' : detail.order.remark; editForm.customer_demand = detail.order.customerDemand || ''; + editForm.revision_reason = ''; // 复制商品明细 editItems.value = (detail.items || []).map(item => ({...item})); } @@ -1179,33 +1188,55 @@ async function saveEdit() { editSaving.value = true; try { - const payload = { - customer_name: editForm.customer_name.trim(), - customer_mobile: editForm.customer_mobile.trim(), - customer_address: editForm.customer_address.trim() || null, - order_source: editForm.order_source.trim() || null, - delivery_type: editForm.delivery_type.trim() || null, - contract_amount: Number(editForm.contract_amount) || 0, - rebate_total: Number(editForm.rebate_total) || 0, - freight_total: Number(editForm.freight_total) || 0, - tax_total: Number(editForm.tax_total) || 0, - other_fee_total: Number(editForm.other_fee_total) || 0, - commission_amount: Number(editForm.commission_amount) || 0, - payment_method: editForm.payment_method.trim() || null, - remark: editForm.remark.trim() || null, - customer_demand: editForm.customer_demand.trim() || null, - items: editItems.value.map(item => ({ - product_id: item.product_id || null, - product_name: item.product_name.trim(), - specification: item.specification?.trim() || '', - unit: item.unit?.trim() || '', - quantity: Number(item.quantity) || 1, - sale_price: Number(item.sale_price) || 0, - cost_price: Number(item.cost_price) || 0, - })), - }; - await updateOrder(detail.orderId, payload); - toast.success('订单已更新'); + const itemsPayload = editItems.value.map(item => ({ + product_id: item.product_id || null, + product_name: item.product_name.trim(), + specification: item.specification?.trim() || '', + unit: item.unit?.trim() || '', + quantity: Number(item.quantity) || 1, + sale_price: Number(item.sale_price) || 0, + cost_price: Number(item.cost_price) || 0, + })); + + const rawStatus = detail.order.rawStatus; + const isDraftOrRejected = ['draft', 'rejected'].includes(rawStatus); + + if (isDraftOrRejected) { + // 草稿/已退回:使用普通更新接口 + const payload = { + customer_name: editForm.customer_name.trim(), + customer_mobile: editForm.customer_mobile.trim(), + customer_address: editForm.customer_address.trim() || null, + order_source: editForm.order_source.trim() || null, + delivery_type: editForm.delivery_type.trim() || null, + contract_amount: Number(editForm.contract_amount) || 0, + rebate_total: Number(editForm.rebate_total) || 0, + freight_total: Number(editForm.freight_total) || 0, + tax_total: Number(editForm.tax_total) || 0, + other_fee_total: Number(editForm.other_fee_total) || 0, + commission_amount: Number(editForm.commission_amount) || 0, + payment_method: editForm.payment_method.trim() || null, + remark: editForm.remark.trim() || null, + customer_demand: editForm.customer_demand.trim() || null, + items: itemsPayload, + }; + await updateOrder(detail.orderId, payload); + toast.success('订单已更新'); + } else { + // 已提交/已审批等状态:使用修订接口(自动重算成本) + const payload = { + items: itemsPayload, + revision_reason: editForm.revision_reason?.trim() || '管理员修改', + contract_amount: Number(editForm.contract_amount) || undefined, + rebate_total: Number(editForm.rebate_total) || 0, + freight_total: Number(editForm.freight_total) || 0, + tax_total: Number(editForm.tax_total) || 0, + other_fee_total: Number(editForm.other_fee_total) || 0, + }; + await reviseOrder(detail.orderId, payload); + toast.success('订单已修订,成本已自动重算'); + } + isEditing.value = false; await loadDetail(); await handleSearch(); @@ -1651,7 +1682,7 @@ async function handleApprovalPass() { detailActionBusy.value = false; } } else { - // 非自发订单:需要选择工厂和司机 + // 非自发订单:调用审批接口,后端统一处理工厂分配和物流任务创建 if (!selectedFactoryId.value) { toast.error('请选择下发工厂'); return; @@ -1662,20 +1693,11 @@ async function handleApprovalPass() { } detailActionBusy.value = true; try { - // 先选择工厂并进入待工厂状态 - await changeOrderStatus(detail.orderId, { - target_status: 'pending_factory', + await approveOrder(detail.orderId, { + approve_result: 'pass', + approve_opinion: '审批通过', factory_id: selectedFactoryId.value, - }); - // 再分配司机并创建物流任务 - await createLogisticsTask({ - order_id: detail.orderId, driver_id: Number(selectedDriverId.value), - pickup_address: getFactoryAddress(selectedFactoryId.value) || detail.order.factoryName || '', - delivery_address: detail.order.customerAddress || '', - pickup_content: detail.order.customerName ? `${detail.order.customerName} 订单货物` : '', - quantity: 1, - factory_id: selectedFactoryId.value, }); toast.success('审批通过,已下发工厂并分配司机'); await loadDetail(); @@ -1706,7 +1728,8 @@ async function handleApprovalReject() { // 驳回取消申请 - 使用 cancel-approve 接口 await cancelApproveOrder(detail.orderId, {approve_result: 'refuse', approve_opinion: opinion || '驳回取消'}); } else { - await changeOrderStatus(detail.orderId, {target_status: 'rejected', approve_opinion: opinion || '退回'}); + // 使用审批接口退回,确保审批日志正确记录 + await approveOrder(detail.orderId, {approve_result: 'reject', approve_opinion: opinion || '退回'}); } toast.success(isCancelFlow ? '已驳回取消' : '已退回'); await loadDetail(); diff --git a/frontend/web-admin/src/views/PricingRulesPage.vue b/frontend/web-admin/src/views/PricingRulesPage.vue index c5ad5c8..31bcd07 100644 --- a/frontend/web-admin/src/views/PricingRulesPage.vue +++ b/frontend/web-admin/src/views/PricingRulesPage.vue @@ -214,6 +214,8 @@ + +