| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980 |
- """可替换的 PDF 查询/下载接口;模拟接口返回原始测试 PDF,保留其真实身份。"""
- import hashlib
- from pathlib import Path
- import httpx
- from fastapi import FastAPI, HTTPException
- from fastapi.responses import FileResponse
- def create_mock_report_app(pdf_path):
- source = Path(pdf_path).resolve()
- if not source.is_file() or source.suffix.lower() != '.pdf':
- raise ValueError('模拟报告接口需要已有的测试 PDF 文件')
- sha = hashlib.sha256(source.read_bytes()).hexdigest()
- app = FastAPI(title='主通风机测定报告模拟接口')
- @app.get('/api/main-fan/reports')
- async def reports(mine_code: str, shaft_id: str):
- return {'code': 200, 'data': [{
- 'file_id': 'fixture-' + sha[:16], 'filename': source.name, 'sha256': sha,
- 'download_path': '/api/main-fan/report-files/' + sha + '.pdf',
- 'requested_mine_code': mine_code, 'requested_shaft_id': shaft_id,
- 'is_mock': True, 'binding': 'test_fixture_not_mine_report',
- 'note': '复用既有测试PDF原件;不改写封面、矿名或设备身份,不能视为目标矿的正式报告。'}]}
- @app.get('/api/main-fan/report-files/{name}')
- async def download(name: str):
- if name != sha + '.pdf':
- raise HTTPException(404, '报告文件不存在')
- return FileResponse(source, media_type='application/pdf', filename=source.name)
- return app
- class ReportClient:
- def __init__(self, http):
- self.http = http
- async def list(self, mine_code, shaft_id):
- params = {'mine_code': mine_code, 'shaft_id': shaft_id}
- response = await self.http.get('/api/main-fan/reports', params=params)
- response.raise_for_status()
- body = response.json()
- if str(body.get('code')) not in {'0', '200'} or not isinstance(body.get('data'), list):
- raise ValueError('报告列表接口没有返回成功的文件数组')
- return body['data'], {'method': 'GET', 'path': '/api/main-fan/reports', 'params': params}, body
- async def download(self, record, target):
- path = record.get('download_path', '')
- if not path.startswith('/api/main-fan/report-files/') or '..' in path:
- raise ValueError('报告下载路径不在已配置的文件接口内')
- partial = target.with_suffix('.part')
- target.parent.mkdir(parents=True, exist_ok=True)
- size, digest = 0, hashlib.sha256()
- try:
- async with self.http.stream('GET', path) as response:
- response.raise_for_status()
- if 'pdf' not in response.headers.get('content-type', '').lower():
- raise ValueError('报告下载未返回 PDF')
- with partial.open('wb') as stream:
- async for chunk in response.aiter_bytes():
- size += len(chunk)
- if size > 50 * 1024 * 1024:
- raise ValueError('报告文件超过50MB')
- digest.update(chunk)
- stream.write(chunk)
- if not size or not partial.read_bytes()[:5] == b'%PDF-':
- raise ValueError('报告文件为空或不是PDF原件')
- sha = digest.hexdigest()
- if record.get('sha256') and sha != record['sha256']:
- raise ValueError('报告实际内容hash与接口声明不一致')
- partial.replace(target)
- return sha
- finally:
- partial.unlink(missing_ok=True)
- def mock_report_client(pdf_path):
- return httpx.AsyncClient(transport=httpx.ASGITransport(app=create_mock_report_app(pdf_path)),
- base_url='http://mock-report.local', timeout=30)
|