agricultural_input.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. """
  2. 农业投入Cd通量计算API接口
  3. @description: 提供各个地区的农业投入输入Cd通量预测计算功能
  4. """
  5. from fastapi import APIRouter, HTTPException, Query, Body
  6. from fastapi.responses import FileResponse
  7. from pydantic import BaseModel, Field
  8. from typing import Dict, Any, Optional
  9. import logging
  10. import os
  11. from ..services.agricultural_input_service import AgriculturalInputService
  12. router = APIRouter()
  13. # =============================================================================
  14. # 数据模型定义
  15. # =============================================================================
  16. class VisualizationResponse(BaseModel):
  17. """
  18. 可视化结果响应模型
  19. @description: 绘图接口的响应格式
  20. """
  21. success: bool = Field(..., description="是否成功")
  22. message: str = Field(..., description="响应消息")
  23. data: Optional[Dict[str, Any]] = Field(None, description="可视化结果数据")
  24. files: Optional[Dict[str, str]] = Field(None, description="生成的文件路径")
  25. class CdFluxCalculationRequest(BaseModel):
  26. """
  27. 农业投入Cd通量计算请求模型
  28. @description: 用户提供的计算参数
  29. """
  30. # 镉含量参数 (mg/kg)
  31. f3_nitrogen_cd_content: float = Field(..., description="氮肥镉含量平均值 (mg/kg)", ge=0)
  32. f4_phosphorus_cd_content: float = Field(..., description="磷肥镉含量平均值 (mg/kg)", ge=0)
  33. f5_potassium_cd_content: float = Field(..., description="钾肥镉含量平均值 (mg/kg)", ge=0)
  34. f6_compound_cd_content: float = Field(..., description="复合肥镉含量平均值 (mg/kg)", ge=0)
  35. f7_organic_cd_content: float = Field(..., description="有机肥镉含量平均值 (mg/kg)", ge=0)
  36. f8_pesticide_cd_content: float = Field(..., description="农药镉含量 (mg/kg)", ge=0)
  37. f9_farmyard_cd_content: float = Field(..., description="农家肥镉含量 (mg/kg)", ge=0)
  38. f10_film_cd_content: float = Field(..., description="农膜镉含量 (mg/kg)", ge=0)
  39. # 使用量参数 (t/ha/a)
  40. nf_nitrogen_usage: float = Field(..., description="氮肥单位面积使用量 (t/ha/a)", ge=0)
  41. pf_phosphorus_usage: float = Field(..., description="磷肥单位面积使用量 (t/ha/a)", ge=0)
  42. kf_potassium_usage: float = Field(..., description="钾肥单位面积使用量 (t/ha/a)", ge=0)
  43. cf_compound_usage: float = Field(..., description="复合肥单位面积使用量 (t/ha/a)", ge=0)
  44. of_organic_usage: float = Field(..., description="有机肥单位面积使用量 (t/ha/a)", ge=0)
  45. p_pesticide_usage: float = Field(..., description="农药单位面积使用量 (t/ha/a)", ge=0)
  46. ff_farmyard_usage: float = Field(..., description="农家肥单位面积使用量 (t/ha/a)", ge=0)
  47. af_film_usage: float = Field(..., description="农膜(存留)单位面积使用量 (t/ha/a)", ge=0)
  48. # 可选的标识信息
  49. description: Optional[str] = Field(None, description="计算描述信息")
  50. model_config = {
  51. "json_schema_extra": {
  52. "example": {
  53. "f3_nitrogen_cd_content": 0.12,
  54. "f4_phosphorus_cd_content": 0.85,
  55. "f5_potassium_cd_content": 0.05,
  56. "f6_compound_cd_content": 0.45,
  57. "f7_organic_cd_content": 0.22,
  58. "f8_pesticide_cd_content": 0.08,
  59. "f9_farmyard_cd_content": 0.15,
  60. "f10_film_cd_content": 0.03,
  61. "nf_nitrogen_usage": 0.25,
  62. "pf_phosphorus_usage": 0.15,
  63. "kf_potassium_usage": 0.12,
  64. "cf_compound_usage": 0.30,
  65. "of_organic_usage": 2.50,
  66. "p_pesticide_usage": 0.02,
  67. "ff_farmyard_usage": 1.80,
  68. "af_film_usage": 0.05,
  69. "description": "测试地区农业投入Cd通量计算"
  70. }
  71. }
  72. }
  73. # 设置日志
  74. logger = logging.getLogger(__name__)
  75. # =============================================================================
  76. # 农业投入Cd通量计算接口
  77. # =============================================================================
  78. @router.get("/calculate-by-area",
  79. summary="根据地区计算农业投入Cd通量",
  80. description="根据指定地区从Parameters表中获取数据并计算农业投入输入Cd通量")
  81. async def calculate_cd_flux_by_area(
  82. area: str = Query(..., description="地区名称,如:韶关")
  83. ) -> Dict[str, Any]:
  84. """
  85. 根据地区计算农业投入输入Cd通量
  86. @param area: 地区名称
  87. @returns: 计算结果包括总通量和各项明细
  88. 计算公式:农业投入输入Cd(g/ha/a) = F3*NF + F4*PF + F5*KF + F6*CF + F7*OF + F8*P + F9*FF + F10*AF
  89. """
  90. try:
  91. service = AgriculturalInputService()
  92. result = service.calculate_cd_flux_by_area(area)
  93. if not result["success"]:
  94. raise HTTPException(
  95. status_code=404,
  96. detail=result["message"]
  97. )
  98. return result
  99. except HTTPException:
  100. raise
  101. except Exception as e:
  102. logger.error(f"计算地区 '{area}' 的Cd通量失败: {str(e)}")
  103. raise HTTPException(
  104. status_code=500,
  105. detail=f"计算失败: {str(e)}"
  106. )
  107. @router.get("/calculate-all-areas",
  108. summary="计算所有地区的农业投入Cd通量",
  109. description="计算数据库中所有地区的农业投入输入Cd通量,并提供统计汇总")
  110. async def calculate_all_areas_cd_flux() -> Dict[str, Any]:
  111. """
  112. 计算所有地区的农业投入输入Cd通量
  113. @returns: 所有地区的计算结果,按通量从高到低排序,包含统计汇总
  114. """
  115. try:
  116. service = AgriculturalInputService()
  117. result = service.calculate_all_areas_cd_flux()
  118. if not result["success"]:
  119. raise HTTPException(
  120. status_code=500,
  121. detail=result["message"]
  122. )
  123. return result
  124. except HTTPException:
  125. raise
  126. except Exception as e:
  127. logger.error(f"计算所有地区Cd通量失败: {str(e)}")
  128. raise HTTPException(
  129. status_code=500,
  130. detail=f"计算失败: {str(e)}"
  131. )
  132. @router.get("/available-areas",
  133. summary="获取可用地区列表",
  134. description="获取数据库中所有可用于计算的地区列表")
  135. async def get_available_areas() -> Dict[str, Any]:
  136. """
  137. 获取数据库中所有可用的地区列表
  138. @returns: 可用地区列表
  139. """
  140. try:
  141. service = AgriculturalInputService()
  142. result = service.get_available_areas()
  143. if not result["success"]:
  144. raise HTTPException(
  145. status_code=500,
  146. detail=result["message"]
  147. )
  148. return result
  149. except HTTPException:
  150. raise
  151. except Exception as e:
  152. logger.error(f"获取可用地区列表失败: {str(e)}")
  153. raise HTTPException(
  154. status_code=500,
  155. detail=f"获取失败: {str(e)}"
  156. )
  157. @router.post("/calculate-with-custom-data",
  158. summary="使用自定义数据计算农业投入Cd通量",
  159. description="接收用户提供的参数数据,直接进行农业投入输入Cd通量计算")
  160. async def calculate_cd_flux_with_custom_data(
  161. request: CdFluxCalculationRequest = Body(..., description="计算所需的参数数据")
  162. ) -> Dict[str, Any]:
  163. """
  164. 使用用户提供的自定义数据计算农业投入输入Cd通量
  165. @param request: 包含所有计算参数的请求体
  166. @returns: 计算结果包括总通量和各项明细
  167. 计算公式:农业投入输入Cd(g/ha/a) = F3*NF + F4*PF + F5*KF + F6*CF + F7*OF + F8*P + F9*FF + F10*AF
  168. """
  169. try:
  170. service = AgriculturalInputService()
  171. result = service.calculate_cd_flux_with_custom_data(request)
  172. if not result["success"]:
  173. raise HTTPException(
  174. status_code=400,
  175. detail=result["message"]
  176. )
  177. return result
  178. except HTTPException:
  179. raise
  180. except Exception as e:
  181. logger.error(f"使用自定义数据计算Cd通量失败: {str(e)}")
  182. raise HTTPException(
  183. status_code=500,
  184. detail=f"计算失败: {str(e)}"
  185. )
  186. @router.get("/calculation-formula",
  187. summary="获取计算公式说明",
  188. description="获取农业投入Cd通量的计算公式和参数说明")
  189. async def get_calculation_formula() -> Dict[str, Any]:
  190. """
  191. 获取农业投入Cd通量的计算公式和参数说明
  192. @returns: 公式和参数详细说明
  193. """
  194. try:
  195. formula_info = {
  196. "success": True,
  197. "message": "农业投入Cd通量计算公式",
  198. "data": {
  199. "formula": "农业投入输入Cd(g/ha/a) = F3*NF + F4*PF + F5*KF + F6*CF + F7*OF + F8*P + F9*FF + F10*AF",
  200. "unit": "g/ha/a",
  201. "parameters": {
  202. "F3": "氮肥镉含量平均值(mg/kg)",
  203. "F4": "磷肥镉含量平均值(mg/kg)",
  204. "F5": "钾肥镉含量平均值(mg/kg)",
  205. "F6": "复合肥镉含量平均值(mg/kg)",
  206. "F7": "有机肥镉含量平均值(mg/kg)",
  207. "F8": "农药镉含量(mg/kg)",
  208. "F9": "农家肥镉含量(mg/kg)",
  209. "F10": "农膜镉含量(mg/kg)",
  210. "NF": "氮肥单位面积使用量(t/ha/a)",
  211. "PF": "磷肥单位面积使用量(t/ha/a)",
  212. "KF": "钾肥单位面积使用量(t/ha/a)",
  213. "CF": "复合肥单位面积使用量(t/ha/a)",
  214. "OF": "有机肥单位面积使用量(t/ha/a)",
  215. "P": "农药单位面积使用量(t/ha/a)",
  216. "FF": "农家肥单位面积使用量(t/ha/a)",
  217. "AF": "农膜(存留)单位面积使用量(t/ha/a)"
  218. },
  219. "components": {
  220. "nitrogen_fertilizer": "氮肥贡献量 = F3 × NF",
  221. "phosphorus_fertilizer": "磷肥贡献量 = F4 × PF",
  222. "potassium_fertilizer": "钾肥贡献量 = F5 × KF",
  223. "compound_fertilizer": "复合肥贡献量 = F6 × CF",
  224. "organic_fertilizer": "有机肥贡献量 = F7 × OF",
  225. "pesticide": "农药贡献量 = F8 × P",
  226. "farmyard_manure": "农家肥贡献量 = F9 × FF",
  227. "agricultural_film": "农膜贡献量 = F10 × AF"
  228. }
  229. }
  230. }
  231. return formula_info
  232. except Exception as e:
  233. logger.error(f"获取计算公式失败: {str(e)}")
  234. raise HTTPException(
  235. status_code=500,
  236. detail=f"获取失败: {str(e)}"
  237. )
  238. # =============================================================================
  239. # 农业投入Cd通量可视化接口
  240. # =============================================================================
  241. @router.get("/visualize",
  242. summary="生成农业投入Cd通量可视化图表",
  243. description="计算农业投入Cd通量并生成栅格地图,参数固定使用韶关数据,area用于地图边界")
  244. async def visualize_agricultural_input(
  245. area: str = Query(..., description="地区名称,如:乐昌市(用于地图边界,参数固定使用韶关)"),
  246. level: str = Query(..., description="行政层级,必须为: county | city | province"),
  247. colormap: str = Query("green_yellow_red_purple", description="色彩方案"),
  248. resolution_factor: float = Query(4.0, description="分辨率因子(默认4.0,更快)"),
  249. enable_interpolation: bool = Query(False, description="是否启用空间插值(默认关闭以提升性能)"),
  250. cleanup_intermediate: bool = Query(True, description="是否清理中间文件(默认是)")
  251. ):
  252. """
  253. 生成农业投入Cd通量可视化图表
  254. @param area: 地区名称(用于地图边界,参数数据固定使用韶关)
  255. @param level: 行政层级
  256. @returns: 栅格地图文件
  257. 功能包括:
  258. 1. 计算农业投入Cd通量(参数固定使用韶关)
  259. 2. 生成指定地区边界的栅格地图
  260. 3. 直接返回图片文件
  261. """
  262. try:
  263. service = AgriculturalInputService()
  264. # 行政层级校验(不允许模糊)
  265. if level not in ("county", "city", "province"):
  266. raise HTTPException(status_code=400, detail="参数 level 必须为 'county' | 'city' | 'province'")
  267. # 计算农业投入Cd通量(使用韶关参数,area仅用于边界)
  268. calc_result = service.calculate_cd_flux_for_visualization(area)
  269. if not calc_result["success"]:
  270. raise HTTPException(
  271. status_code=404,
  272. detail=calc_result["message"]
  273. )
  274. # 获取包含坐标的结果数据
  275. results_with_coords = service.get_coordinates_for_results(calc_result["data"], area)
  276. if not results_with_coords:
  277. raise HTTPException(
  278. status_code=404,
  279. detail=f"未找到地区 '{area}' 的坐标数据,无法生成可视化"
  280. )
  281. # 创建可视化
  282. visualization_files = service.create_agricultural_input_visualization(
  283. area=area,
  284. level=level,
  285. calculation_type="agricultural_input",
  286. results_with_coords=results_with_coords,
  287. colormap=colormap,
  288. resolution_factor=resolution_factor,
  289. enable_interpolation=enable_interpolation,
  290. cleanup_intermediate=cleanup_intermediate
  291. )
  292. # 检查地图文件是否生成成功
  293. map_file = visualization_files.get("map")
  294. if not map_file or not os.path.exists(map_file):
  295. raise HTTPException(status_code=500, detail="地图文件生成失败")
  296. return FileResponse(
  297. path=map_file,
  298. filename=f"{area}_agricultural_input_cd_flux_map.jpg",
  299. media_type="image/jpeg"
  300. )
  301. except HTTPException:
  302. raise
  303. except Exception as e:
  304. logger.error(f"生成地区 '{area}' 的农业投入Cd通量可视化失败: {str(e)}")
  305. raise HTTPException(
  306. status_code=500,
  307. detail=f"可视化生成失败: {str(e)}"
  308. )
  309. @router.get("/export-data",
  310. summary="导出农业投入Cd通量计算数据",
  311. description="导出指定地区的农业投入Cd通量计算结果为CSV文件")
  312. async def export_agricultural_input_data(
  313. area: str = Query(..., description="地区名称,如:韶关")
  314. ) -> Dict[str, Any]:
  315. """
  316. 导出农业投入Cd通量计算数据
  317. @param area: 地区名称
  318. @returns: 导出结果和文件路径
  319. """
  320. try:
  321. service = AgriculturalInputService()
  322. # 计算农业投入Cd通量(使用韶关参数,area仅用于边界)
  323. calc_result = service.calculate_cd_flux_for_visualization(area)
  324. if not calc_result["success"]:
  325. raise HTTPException(
  326. status_code=404,
  327. detail=calc_result["message"]
  328. )
  329. # 导出数据
  330. csv_path = service.export_results_to_csv(calc_result["data"])
  331. return {
  332. "success": True,
  333. "message": f"地区 '{area}' 的农业投入Cd通量数据导出成功",
  334. "data": {
  335. "area": area,
  336. "calculation_type": "agricultural_input",
  337. "exported_file": csv_path,
  338. "total_cd_flux": calc_result["data"]["total_cd_flux"],
  339. "unit": calc_result["data"]["unit"]
  340. }
  341. }
  342. except HTTPException:
  343. raise
  344. except Exception as e:
  345. logger.error(f"导出地区 '{area}' 的农业投入Cd通量数据失败: {str(e)}")
  346. raise HTTPException(
  347. status_code=500,
  348. detail=f"数据导出失败: {str(e)}"
  349. )