config_api.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. from fastapi import APIRouter, File, Body, HTTPException, status
  2. from fastapi.responses import FileResponse, JSONResponse
  3. from ..core.config import settings
  4. import json
  5. from app.core.logger import get_logger
  6. logger = get_logger(__name__)
  7. router = APIRouter(tags=["Config"])
  8. def compare_json_structure(template: dict, data: dict, path: str = "") -> (bool, str):
  9. """
  10. 递归比较两个字典的结构(键),忽略值。
  11. 如果结构不匹配,返回 False 和错误原因。
  12. :param template: 模板字典(原始配置)
  13. :param data: 新的字典(待验证的配置)
  14. :param path: 用于错误报告的当前递归路径
  15. :return: 一个元组 (is_match: bool, reason: str)
  16. """
  17. template_keys = set(template.keys())
  18. data_keys = set(data.keys())
  19. # 检查当前层的键是否完全匹配
  20. if template_keys != data_keys:
  21. missing_keys = template_keys - data_keys
  22. extra_keys = data_keys - template_keys
  23. error_msg = f"结构不匹配。在路径 '{path or 'root'}' "
  24. if missing_keys:
  25. error_msg += f"缺少字段: {missing_keys}。"
  26. if extra_keys:
  27. error_msg += f"存在多余字段: {extra_keys}。"
  28. return False, error_msg
  29. # 递归检查所有子字典
  30. for key in template_keys:
  31. if isinstance(template[key], dict) and isinstance(data[key], dict):
  32. # 如果两个值都是字典,则递归深入
  33. is_match, reason = compare_json_structure(template[key], data[key], path=f"{path}.{key}" if path else key)
  34. if not is_match:
  35. return False, reason
  36. elif isinstance(template[key], dict) or isinstance(data[key], dict):
  37. # 如果只有一个是字典,说明结构已改变(例如,一个对象被替换成了字符串)
  38. current_path = f"{path}.{key}" if path else key
  39. return False, f"结构不匹配。字段 '{current_path}' 的类型已从字典变为非字典(或反之)。"
  40. return True, "结构匹配"
  41. @router.get("/scoring_config", summary="获取评分配置")
  42. async def get_scoring_config():
  43. """
  44. 读取并返回 scoring_config.json 文件的内容。
  45. """
  46. if not settings.SCORE_CONFIG_PATH.exists():
  47. logger.error(f"评分配置文件未找到: {settings.SCORE_CONFIG_PATH}")
  48. raise HTTPException(
  49. status_code=status.HTTP_404_NOT_FOUND,
  50. detail="评分配置文件未找到"
  51. )
  52. try:
  53. with open(settings.SCORE_CONFIG_PATH, 'r', encoding='utf-8') as f:
  54. config_data = json.load(f)
  55. return config_data
  56. except json.JSONDecodeError:
  57. logger.error(f"评分配置文件格式错误,无法解析: {settings.SCORE_CONFIG_PATH}")
  58. raise HTTPException(
  59. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  60. detail="评分配置文件格式错误"
  61. )
  62. except Exception as e:
  63. logger.error(f"读取配置文件时发生未知错误: {e}")
  64. raise HTTPException(
  65. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  66. detail=f"读取配置文件时发生未知错误: {e}"
  67. )
  68. @router.put("/scoring_config", summary="更新评分配置")
  69. async def update_scoring_config(new_config: dict = Body(...)):
  70. """
  71. 接收新的JSON配置,验证其结构与现有配置完全一致后,覆盖保存。
  72. 只允许修改值,不允许增、删、改任何字段名。
  73. """
  74. # 1. 检查并读取当前的配置文件作为模板
  75. if not settings.SCORE_CONFIG_PATH.exists():
  76. logger.error(f"尝试更新一个不存在的配置文件: {settings.SCORE_CONFIG_PATH}")
  77. raise HTTPException(
  78. status_code=status.HTTP_404_NOT_FOUND,
  79. detail="无法更新,因为原始配置文件未找到"
  80. )
  81. try:
  82. with open(settings.SCORE_CONFIG_PATH, 'r', encoding='utf-8') as f:
  83. current_config = json.load(f)
  84. except Exception as e:
  85. logger.error(f"更新前读取原始配置文件失败: {e}")
  86. raise HTTPException(
  87. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  88. detail=f"读取原始配置文件失败: {e}"
  89. )
  90. # 2. 比较新旧配置的结构
  91. is_valid, reason = compare_json_structure(current_config, new_config)
  92. if not is_valid:
  93. logger.warning(f"更新评分配置失败,结构校验未通过: {reason}")
  94. raise HTTPException(
  95. status_code=status.HTTP_400_BAD_REQUEST,
  96. detail=f"配置更新失败。{reason} 请确保只修改数值,不要添加、删除或重命名字段。"
  97. )
  98. # 3. 结构验证通过,写入新文件
  99. try:
  100. with open(settings.SCORE_CONFIG_PATH, 'w', encoding='utf-8') as f:
  101. # 使用 indent=2 格式化输出,方便人工阅读
  102. json.dump(new_config, f, indent=2, ensure_ascii=False)
  103. logger.info("评分配置文件已成功更新。")
  104. return JSONResponse(
  105. status_code=status.HTTP_200_OK,
  106. content={"message": "配置已成功更新"}
  107. )
  108. except Exception as e:
  109. logger.error(f"写入新配置文件时发生错误: {e}")
  110. raise HTTPException(
  111. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  112. detail=f"保存新配置文件时发生错误: {e}"
  113. )