report_api.py 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. """可替换的 PDF 查询/下载接口;模拟接口返回原始测试 PDF,保留其真实身份。"""
  2. import hashlib
  3. from pathlib import Path
  4. import httpx
  5. from fastapi import FastAPI, HTTPException
  6. from fastapi.responses import FileResponse
  7. def create_mock_report_app(pdf_path):
  8. source = Path(pdf_path).resolve()
  9. if not source.is_file() or source.suffix.lower() != '.pdf':
  10. raise ValueError('模拟报告接口需要已有的测试 PDF 文件')
  11. sha = hashlib.sha256(source.read_bytes()).hexdigest()
  12. app = FastAPI(title='主通风机测定报告模拟接口')
  13. @app.get('/api/main-fan/reports')
  14. async def reports(mine_code: str, shaft_id: str):
  15. return {'code': 200, 'data': [{
  16. 'file_id': 'fixture-' + sha[:16], 'filename': source.name, 'sha256': sha,
  17. 'download_path': '/api/main-fan/report-files/' + sha + '.pdf',
  18. 'requested_mine_code': mine_code, 'requested_shaft_id': shaft_id,
  19. 'is_mock': True, 'binding': 'test_fixture_not_mine_report',
  20. 'note': '复用既有测试PDF原件;不改写封面、矿名或设备身份,不能视为目标矿的正式报告。'}]}
  21. @app.get('/api/main-fan/report-files/{name}')
  22. async def download(name: str):
  23. if name != sha + '.pdf':
  24. raise HTTPException(404, '报告文件不存在')
  25. return FileResponse(source, media_type='application/pdf', filename=source.name)
  26. return app
  27. class ReportClient:
  28. def __init__(self, http):
  29. self.http = http
  30. async def list(self, mine_code, shaft_id):
  31. params = {'mine_code': mine_code, 'shaft_id': shaft_id}
  32. response = await self.http.get('/api/main-fan/reports', params=params)
  33. response.raise_for_status()
  34. body = response.json()
  35. if str(body.get('code')) not in {'0', '200'} or not isinstance(body.get('data'), list):
  36. raise ValueError('报告列表接口没有返回成功的文件数组')
  37. return body['data'], {'method': 'GET', 'path': '/api/main-fan/reports', 'params': params}, body
  38. async def download(self, record, target):
  39. path = record.get('download_path', '')
  40. if not path.startswith('/api/main-fan/report-files/') or '..' in path:
  41. raise ValueError('报告下载路径不在已配置的文件接口内')
  42. partial = target.with_suffix('.part')
  43. target.parent.mkdir(parents=True, exist_ok=True)
  44. size, digest = 0, hashlib.sha256()
  45. try:
  46. async with self.http.stream('GET', path) as response:
  47. response.raise_for_status()
  48. if 'pdf' not in response.headers.get('content-type', '').lower():
  49. raise ValueError('报告下载未返回 PDF')
  50. with partial.open('wb') as stream:
  51. async for chunk in response.aiter_bytes():
  52. size += len(chunk)
  53. if size > 50 * 1024 * 1024:
  54. raise ValueError('报告文件超过50MB')
  55. digest.update(chunk)
  56. stream.write(chunk)
  57. if not size or not partial.read_bytes()[:5] == b'%PDF-':
  58. raise ValueError('报告文件为空或不是PDF原件')
  59. sha = digest.hexdigest()
  60. if record.get('sha256') and sha != record['sha256']:
  61. raise ValueError('报告实际内容hash与接口声明不一致')
  62. partial.replace(target)
  63. return sha
  64. finally:
  65. partial.unlink(missing_ok=True)
  66. def mock_report_client(pdf_path):
  67. return httpx.AsyncClient(transport=httpx.ASGITransport(app=create_mock_report_app(pdf_path)),
  68. base_url='http://mock-report.local', timeout=30)