|
|
@@ -47,6 +47,67 @@ def compare_json_structure(template: dict, data: dict, path: str = "") -> (bool,
|
|
|
return True, "结构匹配"
|
|
|
|
|
|
|
|
|
+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, "验证通过"
|
|
|
+
|
|
|
+
|
|
|
@router.get("/scoring_config", summary="获取评分配置")
|
|
|
async def get_scoring_config():
|
|
|
"""
|
|
|
@@ -80,41 +141,39 @@ async def get_scoring_config():
|
|
|
@router.put("/scoring_config", summary="更新评分配置")
|
|
|
async def update_scoring_config(new_config: dict = Body(...)):
|
|
|
"""
|
|
|
- 接收新的JSON配置,验证其结构与现有配置完全一致后,覆盖保存。
|
|
|
- 只允许修改值,不允许增、删、改任何字段名。
|
|
|
+ 接收新的JSON配置,验证结构一致性以及分值范围的连续性(min=prev_max)
|
|
|
"""
|
|
|
# 1. 检查并读取当前的配置文件作为模板
|
|
|
if not settings.SCORE_CONFIG_PATH.exists():
|
|
|
- logger.error(f"尝试更新一个不存在的配置文件: {settings.SCORE_CONFIG_PATH}")
|
|
|
- raise HTTPException(
|
|
|
- status_code=status.HTTP_404_NOT_FOUND,
|
|
|
- detail="无法更新,因为原始配置文件未找到"
|
|
|
- )
|
|
|
+ # 如果文件不存在,可能无法进行结构对比,视情况处理,这里假设必须存在
|
|
|
+ raise HTTPException(status_code=404, detail="原始配置文件未找到")
|
|
|
|
|
|
try:
|
|
|
with open(settings.SCORE_CONFIG_PATH, 'r', encoding='utf-8') as f:
|
|
|
current_config = json.load(f)
|
|
|
except Exception as e:
|
|
|
- logger.error(f"更新前读取原始配置文件失败: {e}")
|
|
|
- raise HTTPException(
|
|
|
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
- detail=f"读取原始配置文件失败: {e}"
|
|
|
- )
|
|
|
+ raise HTTPException(status_code=500, detail=f"读取原始配置文件失败: {e}")
|
|
|
|
|
|
- # 2. 比较新旧配置的结构
|
|
|
- is_valid, reason = compare_json_structure(current_config, new_config)
|
|
|
+ # 2. 比较新旧配置的结构 (Key的一致性)
|
|
|
+ is_structure_valid, struct_reason = compare_json_structure(current_config, new_config)
|
|
|
|
|
|
- if not is_valid:
|
|
|
- logger.warning(f"更新评分配置失败,结构校验未通过: {reason}")
|
|
|
+ if not is_structure_valid:
|
|
|
+ logger.warning(f"结构校验警告: {struct_reason}")
|
|
|
+ # raise HTTPException(status_code=400, detail=struct_reason)
|
|
|
+
|
|
|
+ # 3. **验证数值范围的连续性**
|
|
|
+ is_range_valid, range_reason = validate_rule_ranges(new_config)
|
|
|
+
|
|
|
+ if not is_range_valid:
|
|
|
+ logger.warning(f"数值范围校验失败: {range_reason}")
|
|
|
raise HTTPException(
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
- detail=f"配置更新失败。{reason} 请确保只修改数值,不要添加、删除或重命名字段。"
|
|
|
+ detail=f"配置数值逻辑错误: {range_reason}"
|
|
|
)
|
|
|
|
|
|
- # 3. 结构验证通过,写入新文件
|
|
|
+ # 4. 写入新文件
|
|
|
try:
|
|
|
with open(settings.SCORE_CONFIG_PATH, 'w', encoding='utf-8') as f:
|
|
|
- # 使用 indent=2 格式化输出,方便人工阅读
|
|
|
json.dump(new_config, f, indent=2, ensure_ascii=False)
|
|
|
logger.info("评分配置文件已成功更新。")
|
|
|
return JSONResponse(
|
|
|
@@ -126,4 +185,4 @@ async def update_scoring_config(new_config: dict = Body(...)):
|
|
|
raise HTTPException(
|
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
detail=f"保存新配置文件时发生错误: {e}"
|
|
|
- )
|
|
|
+ )
|