"""配置驱动的报价计算引擎。 所有产品差异由 product_pricing_rule 表的 JSON 配置决定, 引擎本身不包含任何产品特定的 if/else 分支。 """ import json import re from decimal import Decimal, ROUND_HALF_UP class PricingEngine: """读取 PricingRule 配置,执行公式求值、附加费计算、详情构建。 所有产品差异由 product_pricing_rule 表的 JSON 配置驱动, 引擎本身不包含任何产品特定的 if/else 分支。 """ # ------------------------------------------------------------------ # 公开方法 # ------------------------------------------------------------------ def calculate(self, rule, user_inputs: dict, product_attrs: dict | None = None) -> dict: """执行完整报价计算。 流程:单位换算 -> 构建公式上下文 -> 公式求值 -> 附加费计算 -> 构建公式说明。 Args: rule: ProductPricingRule ORM 对象(含 formula_expr 等字段) user_inputs: 前端传入 {"length": 2, "width": 1.2, "length_unit": "m", ...} product_attrs: 产品属性 {"thickness": 0.08} 用于阈值判断 Returns: { "base_cost", "surcharge_items", "total_surcharge", "cost_price", "formula_detail", "formula_detail_steps" } 被调用路由: pricing.py - POST /pricing/calculate """ product_attrs = product_attrs or {} # 1. 单位换算 → 统一为米 normalized = self._normalize_inputs(rule, user_inputs) area_sqm = normalized.get("length_m", 0) * normalized.get("width_m", 0) # 2. 构建公式上下文并求值 context = { "input": normalized, "rule": {"base_unit_price": float(rule.base_unit_price)}, "const": self._safe_load_json(rule.formula_constants) or {}, "product": product_attrs, } base_cost = self._evaluate_formula(rule.formula_expr or "0", context) # 3. 计算附加费 surcharges = self._calculate_surcharges( rule.surcharge_json, user_inputs, normalized, area_sqm, product_attrs, ) total_surcharge = round(sum(s["amount"] for s in surcharges), 2) cost_price = round(base_cost + total_surcharge, 2) # 4. 构建公式说明 formula_detail = self._build_formula_detail(rule, normalized, base_cost, surcharges) # 5. 税计算 tax_rate = float(getattr(rule, "tax_rate", 0) or 0) 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) else: price_ex_tax = cost_price tax_amount = 0 price_in_tax = cost_price return { "base_cost": round(base_cost, 2), "surcharge_items": surcharges, "total_surcharge": total_surcharge, "cost_price": cost_price, "formula_detail": formula_detail, "tax_rate": tax_rate, "price_ex_tax": price_ex_tax, "tax_amount": tax_amount, "price_in_tax": price_in_tax, } def get_available_surcharge_options(self, rule) -> list[dict]: """返回定价规则中可选的附加费配置列表。 Args: rule: ProductPricingRule ORM 对象 Returns: 附加费选项列表,每项包含 key、name、method、price 等 被调用路由: pricing.py - GET /pricing/surcharge-options/{rule_id} """ surcharge = self._safe_load_json(rule.surcharge_json) or {} return [ {"key": key, "name": cfg.get("name", key), "method": cfg.get("method", ""), "price": cfg.get("price", 0), "needs_input": cfg.get("needs_input", False), "input_key": cfg.get("input_key", key)} for key, cfg in surcharge.items() ] # ------------------------------------------------------------------ # 单位换算 # ------------------------------------------------------------------ _UNIT_TO_M = {"m": 1.0, "cm": 0.01, "mm": 0.001} def _normalize_inputs(self, rule, user_inputs: dict) -> dict: """将前端输入转换为统一的米制数值。 根据 pricing_inputs 声明解析字段,自动读取 _unit 后缀进行单位换算。 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: key = field.get("key", "") val = user_inputs.get(key) if val is None or val == "": normalized[key] = 0 continue val = float(val) if field.get("type") == "boolean": 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) normalized[key] = val return normalized # ------------------------------------------------------------------ # 公式求值 # ------------------------------------------------------------------ def _evaluate_formula(self, expr: str, context: dict) -> float: """替换变量 -> 解析条件表达式 -> 安全求值。 支持 $input.xxx、$rule.xxx、$const.xxx、$product.xxx 变量引用, 以及 ${condition} ? a : b 三元条件表达式。 Args: expr: 公式表达式字符串 context: 变量上下文字典 Returns: 计算结果浮点数,异常时返回 0.0 """ resolved = expr # 替换 $input.xxx / $rule.xxx / $const.xxx / $product.xxx 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)) # 解析 ${condition} ? a : b resolved = self._resolve_conditionals(resolved) # 安全求值 try: return round(float(eval(resolved, {"__builtins__": {}}, {"round": round})), 4) except Exception: return 0.0 def _resolve_conditionals(self, expr: str) -> str: """处理 ${condition} ? valueA : valueB 三元表达式。 循环替换直到表达式中不再包含条件表达式(支持嵌套)。 Args: expr: 含条件表达式的公式字符串 Returns: 条件表达式已解析的公式字符串 """ pattern = r"\$\{([^}]+)\}\s*\?\s*([^:]+?)\s*:\s*([^,\s\)]+)" while re.search(pattern, expr): expr = re.sub(pattern, self._eval_one_conditional, expr) return expr def _eval_one_conditional(self, match: re.Match) -> str: """求值单个三元条件表达式。 Args: match: 正则匹配对象,包含条件、真值、假值三个分组 Returns: 条件求值结果对应的值字符串 """ cond = match.group(1).strip() val_true = match.group(2).strip() val_false = match.group(3).strip() try: result = eval(cond, {"__builtins__": {}}, {}) # noqa: S307 return val_true if result else val_false except Exception: return val_false # ------------------------------------------------------------------ # 附加费计算 # ------------------------------------------------------------------ def _calculate_surcharges( self, surcharge_json, user_inputs: dict, normalized: dict, area_sqm: float, product_attrs: dict, ) -> list[dict]: """计算所有启用的附加费项。 支持四种计费方式:按面积(per_sqm)、按数量(per_piece)、 按长度(per_linear_m)、按面积阈值(per_sqm_threshold)。 Args: surcharge_json: 附加费配置 JSON 字符串 user_inputs: 前端原始输入 normalized: 标准化后的输入 area_sqm: 面积(平方米) product_attrs: 产品属性字典 Returns: 附加费计算结果列表,每项包含 key、name、method、amount """ surcharge = self._safe_load_json(surcharge_json) or {} results = [] for key, cfg in surcharge.items(): if not user_inputs.get(f"surcharge_{key}", False): continue method = cfg.get("method", "per_sqm") amount = 0.0 if method == "per_sqm": amount = round(area_sqm * cfg.get("price", 0), 2) elif method == "per_piece": input_key = cfg.get("input_key", key) qty = float(user_inputs.get(input_key, 0) or 0) amount = round(qty * cfg.get("price", 0), 2) elif method == "per_linear_m": input_key = cfg.get("input_key", key) length = float(user_inputs.get(input_key, 0) or 0) amount = round(length * cfg.get("price", 0), 2) elif method == "per_sqm_threshold": threshold = cfg.get("threshold", {}) field_val = float(product_attrs.get(threshold.get("field", ""), 0) or 0) price = threshold["price_true"] if field_val <= threshold.get("lte", 0) else threshold["price_false"] amount = round(area_sqm * price, 2) results.append({ "key": key, "name": cfg.get("name", key), "method": method, "amount": amount, }) return results # ------------------------------------------------------------------ # 公式说明构建 # ------------------------------------------------------------------ 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 Args: rule: 定价规则 ORM 对象 normalized: 标准化后的输入 base_cost: 基础费用 surcharges: 附加费列表 Returns: 公式说明字符串 """ 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}") for s in surcharges: parts.append(f"+ {s['name']} ¥{s['amount']:.2f}") 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) if tax_inclusive: parts.append(f"含税价(税率{tax_rate}%)") else: parts.append(f"+ 税额(税率{tax_rate}%)") return " → ".join(parts) # ------------------------------------------------------------------ # 工具方法 # ------------------------------------------------------------------ @staticmethod def _safe_load_json(text: str | None) -> dict | list | None: """安全解析 JSON 字符串。 Args: text: JSON 字符串 Returns: 解析后的 dict/list,空值或解析失败时返回 None """ if not text: return None try: return json.loads(text) except (json.JSONDecodeError, TypeError): return None @staticmethod def _to_str(v) -> str: """将值转换为字符串,布尔值特殊处理为 True/False。 Args: v: 任意值 Returns: 字符串表示 """ if isinstance(v, bool): return "True" if v else "False" return str(v) # 模块级单例 pricing_engine = PricingEngine()