def validate_rule_ranges(data: dict, path: str = "") -> (bool, str): """ 递归检查配置中看起来像评分规则的列表。 规则: 1. 列表中的元素必须包含 min, max, deduction。 2. 第一个元素的 min 必须为 0。 3. 当前元素的 min 必须等于上一个元素的 max。 """ if isinstance(data, dict): for key, value in data.items(): is_valid, error = validate_rule_ranges(value, path=f"{path}.{key}" if path else key) if not is_valid: return False, error elif isinstance(data, list): # 检查这是否是一个规则列表(通过检查第一个元素是否包含特定key) if len(data) > 0 and isinstance(data[0], dict) and "min" in data[0] and "max" in data[0] and "deduction" in \ data[0]: current_path = path # 1. 检查首项 min 是否为 0 first_min = data[0].get("min") if first_min != 0: return False, f"范围错误 [{current_path}]: 第一个区间的 min 必须为 0,当前为 {first_min}" # 2. 检查连续性 prev_max = None for idx, rule in enumerate(data): current_min = rule.get("min") current_max = rule.get("max") # 处理 inf 字符串 if current_max == "inf": current_max_val = float("inf") else: try: current_max_val = float(current_max) except (ValueError, TypeError): return False, f"数值错误 [{current_path}]: max 值 '{current_max}' 无效" try: current_min_val = float(current_min) except (ValueError, TypeError): return False, f"数值错误 [{current_path}]: min 值 '{current_min}' 无效" # 检查连续性 (除了第一个元素) if idx > 0: # 使用一个微小的容差处理浮点数比较,或者直接相等 if prev_max == "inf": return False, f"逻辑错误 [{current_path}]: 'inf' 只能出现在最后一个区间的 max" prev_max_val = float(prev_max) if prev_max != "inf" else float("inf") if current_min_val != prev_max_val: return False, f"不连续错误 [{current_path}]: 第 {idx + 1} 项的 min ({current_min}) 不等于上一项的 max ({prev_max})" prev_max = current_max return True, "验证通过" if __name__ == '__main__': new_config = { "base_score": 10, "corner": { "rules": { "wear_area": [ { "min": 0, "max": 0.05, "deduction": -0.1 }, { "min": 0.05, "max": 0.1, "deduction": -0.5 }, { "min": 0.1, "max": 0.25, "deduction": -1.5 }, { "min": 0.25, "max": 0.5, "deduction": -3 }, { "min": 0.5, "max": "inf", "deduction": -5 } ], "loss_area": [ { "min": 0, "max": 0.05, "deduction": -0.1 }, { "min": 0.05, "max": 0.1, "deduction": -0.5 }, { "min": 0.1, "max": 0.25, "deduction": -1.5 }, { "min": 0.25, "max": 0.5, "deduction": -3 }, { "min": 0.5, "max": "inf", "deduction": -5 } ] }, "front_weights": { "wear_area": 0.3, "loss_area": 0.7 }, "back_weights": { "wear_area": 0.3, "loss_area": 0.7 }, "final_weights": { "front": 0.7, "back": 0.3 } }, "edge": { "rules": { "wear_area": [ { "min": 0, "max": 0.05, "deduction": -0.1 }, { "min": 0.05, "max": 0.1, "deduction": -0.5 }, { "min": 0.1, "max": 0.25, "deduction": -1.5 }, { "min": 0.25, "max": 0.5, "deduction": -3 }, { "min": 0.5, "max": "inf", "deduction": -5 } ], "loss_area": [ { "min": 0, "max": 0.05, "deduction": -0.1 }, { "min": 0.05, "max": 0.1, "deduction": -0.5 }, { "min": 0.1, "max": 0.25, "deduction": -1.5 }, { "min": 0.25, "max": 0.5, "deduction": -3 }, { "min": 0.5, "max": "inf", "deduction": -5 } ] }, "front_weights": { "wear_area": 0.4, "loss_area": 0.6 }, "back_weights": { "wear_area": 0.4, "loss_area": 0.6 }, "final_weights": { "front": 0.7, "back": 0.3 } }, "face": { "rules": { "wear_area": [ { "min": 0, "max": 0.05, "deduction": -0.1 }, { "min": 0.05, "max": 0.1, "deduction": -0.5 }, { "min": 0.1, "max": 0.25, "deduction": -1.5 }, { "min": 0.25, "max": 0.5, "deduction": -3 }, { "min": 0.5, "max": "inf", "deduction": -5 } ], "pit_area": [ { "min": 0, "max": 0.05, "deduction": -0.1 }, { "min": 0.05, "max": 0.1, "deduction": -0.5 }, { "min": 0.1, "max": 0.25, "deduction": -1.5 }, { "min": 0.25, "max": 0.5, "deduction": -3 }, { "min": 0.5, "max": "inf", "deduction": -5 } ], "stain_area": [ { "min": 0, "max": 0.05, "deduction": -0.1 }, { "min": 0.05, "max": 0.1, "deduction": -0.5 }, { "min": 0.1, "max": 0.25, "deduction": -1.5 }, { "min": 0.25, "max": 0.5, "deduction": -3 }, { "min": 0.5, "max": "inf", "deduction": -5 } ], "scratch_length": [ { "min": 0, "max": 1, "deduction": -0.1 }, { "min": 1, "max": 2, "deduction": -0.5 }, { "min": 2, "max": 5, "deduction": -1 }, { "min": 5, "max": 10, "deduction": -2 }, { "min": 10, "max": 20, "deduction": -3 }, { "min": 20, "max": 50, "deduction": -4 }, { "min": 50, "max": "inf", "deduction": -5 } ] }, "coefficients": { "wear_area": 0.25, "scratch_length": 0.25, "dent_area": 0.25, "stain_area": 0.25 }, "final_weights": { "front": 0.75, "back": 0.25 } }, "centering": { "front": { "rules": [ { "min": 0, "max": 52, "deduction": 0 }, { "min": 52, "max": 55, "deduction": -0.5 }, { "min": 55, "max": 60, "deduction": -1 }, { "min": 60, "max": 62.5, "deduction": -1.5 }, { "min": 62.5, "max": 65, "deduction": -2 }, { "min": 65, "max": 67.5, "deduction": -2.5 }, { "min": 67.5, "max": 70, "deduction": -3 }, { "min": 70, "max": 72.5, "deduction": -3.5 }, { "min": 72.5, "max": 75, "deduction": -4 }, { "min": 75, "max": 77.5, "deduction": -4.5 }, { "min": 77.5, "max": 80, "deduction": -5 }, { "min": 80, "max": 82.5, "deduction": -5.5 }, { "min": 82.5, "max": 85, "deduction": -6 }, { "min": 85, "max": 87.5, "deduction": -6.5 }, { "min": 87.5, "max": 90, "deduction": -7 }, { "min": 90, "max": 92.5, "deduction": -7.5 }, { "min": 92.5, "max": 95, "deduction": -8 }, { "min": 95, "max": 97.5, "deduction": -8.5 }, { "min": 97.5, "max": "inf", "deduction": -9 } ], "coefficients": { "horizontal": 1.2, "vertical": 0.9 } }, "back": { "rules": [ { "min": 0, "max": 60, "deduction": -0.5 }, { "min": 60, "max": 70, "deduction": -1 }, { "min": 70, "max": 75, "deduction": -1.5 }, { "min": 75, "max": 85, "deduction": -2 }, { "min": 85, "max": 95, "deduction": -2.5 }, { "min": 95, "max": "inf", "deduction": -3 } ], "coefficients": { "horizontal": 1.2, "vertical": 0.9 } }, "final_weights": { "front": 0.75, "back": 0.25 } }, "card": { "PSA": { "face": 0.35, "corner": 0.3, "edge": 0.1, "center": 0.25 }, "BGS": { "face": 0.3, "corner": 0.25, "edge": 0.1, "center": 0.25 } } } is_range_valid, range_reason = validate_rule_ranges(new_config) print(is_range_valid, range_reason)