Files
Docker_Compose/480T/market_api/market_api/main.py
T
2026-07-26 00:30:28 +08:00

493 lines
16 KiB
Python
Executable File

from __future__ import annotations
from pathlib import Path
from shutil import copyfileobj
from tempfile import NamedTemporaryFile
from fastapi import FastAPI, File, HTTPException, Request, UploadFile
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from market_api.award.common import build_ct_filter
from market_api.award.competitor_large_deals import (
CompetitorLargeDealsRepository,
build_competitor_large_deals_summary,
)
from market_api.award.industry import (
AwardIndustryRepository,
build_award_industry_summary,
)
from market_api.award.overall import (
AwardOverallRepository,
build_award_overall_summary,
)
from market_api.award.region import (
AwardRegionRepository,
build_award_region_summary,
validate_award_region_scope,
)
from market_api.core.config import get_settings
from market_api.db.postgres import connect
from market_api.importer.service import import_monthly_excel, is_allowed_upload_filename
from market_api.tender.class_trend import (
TenderClassTrendRepository,
build_class_trend_summary,
)
from market_api.tender.customer_139 import (
TenderCustomer139Repository,
build_customer_139_key_projects_summary,
build_customer_139_summary,
)
from market_api.tender.industry import TenderIndustryRepository, build_industry_summary
from market_api.tender.classification import (
TenderClassRepository,
build_tender_class_summary,
)
from market_api.tender.major_buyers import (
TenderMajorBuyersRepository,
build_major_buyers_summary,
)
from market_api.tender.overall import (
TenderOverallRepository,
build_overall_summary,
build_scope_filter,
normalize_month,
)
from market_api.tender.region import (
TenderRegionRepository,
build_region_summary,
validate_region_scope,
)
from market_api.tender.time import TenderTimeRepository, build_tender_time_summary
app = FastAPI(title="Market API", version="0.1.0")
@app.exception_handler(HTTPException)
def handle_http_exception(_request: Request, exc: HTTPException) -> JSONResponse:
detail = exc.detail if isinstance(exc.detail, str) else "请求处理失败"
return JSONResponse(
status_code=exc.status_code,
content={"错误信息": detail},
headers=exc.headers,
)
@app.exception_handler(RequestValidationError)
def handle_request_validation_error(
_request: Request,
exc: RequestValidationError,
) -> JSONResponse:
return JSONResponse(
status_code=422,
content={
"错误信息": "请求参数校验失败",
"错误详情": [
{
"位置": " -> ".join(str(part) for part in error.get("loc", [])),
"提示": error.get("msg", ""),
"类型": error.get("type", ""),
}
for error in exc.errors()
],
},
)
@app.get("/health")
def health() -> dict[str, str]:
return {"状态": "正常"}
@app.get("/tender/overall")
def tender_overall(
year: int,
month: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = TenderOverallRepository(conn, settings.schema)
return build_overall_summary(repository, year, month, scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/tender/industry")
def tender_industry(
year: int,
month: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = TenderIndustryRepository(conn, settings.schema)
return build_industry_summary(repository, year, month, scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/tender/region")
def tender_region(
year: int,
month: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
validate_region_scope(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = TenderRegionRepository(conn, settings.schema)
return build_region_summary(repository, year, month, scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/tender/major-buyers")
def tender_major_buyers(
year: int,
month: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = TenderMajorBuyersRepository(conn, settings.schema)
return build_major_buyers_summary(repository, year, month, scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/tender/class")
def tender_class(
year: int,
month: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = TenderClassRepository(conn, settings.schema)
return build_tender_class_summary(repository, year, month, scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/tender/class-trend")
def tender_class_trend(
year: int,
month: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = TenderClassTrendRepository(conn, settings.schema)
return build_class_trend_summary(repository, year, month, scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/tender/time")
def tender_time(
year: int,
month: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = TenderTimeRepository(conn, settings.schema)
return build_tender_time_summary(repository, year, month, scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/tender/customer-139")
def tender_customer_139(
year: int,
month: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = TenderCustomer139Repository(conn, settings.schema)
return build_customer_139_summary(repository, year, month, scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/tender/customer-139/key-projects")
def tender_customer_139_key_projects(
year: int,
month: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = TenderCustomer139Repository(conn, settings.schema)
return build_customer_139_key_projects_summary(
repository,
year,
month,
scope,
scope_value,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/award/overall")
def award_overall(
year: int,
month: str,
ct: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_ct_filter(ct)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = AwardOverallRepository(conn, settings.schema)
return build_award_overall_summary(
repository,
year,
month,
ct,
scope,
scope_value,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/award/industry")
def award_industry(
year: int,
month: str,
ct: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_ct_filter(ct)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = AwardIndustryRepository(conn, settings.schema)
return build_award_industry_summary(
repository,
year,
month,
ct,
scope,
scope_value,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/award/region")
def award_region(
year: int,
month: str,
ct: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_ct_filter(ct)
validate_award_region_scope(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = AwardRegionRepository(conn, settings.schema)
return build_award_region_summary(
repository,
year,
month,
ct,
scope,
scope_value,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.get("/award/competitor-large-deals")
def award_competitor_large_deals(
year: int,
month: str,
ct: str,
scope: str,
scope_value: str | None = None,
) -> dict[str, object]:
try:
normalize_month(month)
build_ct_filter(ct)
build_scope_filter(scope, scope_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
settings = get_settings()
try:
with connect(settings.database) as conn:
repository = CompetitorLargeDealsRepository(conn, settings.schema)
return build_competitor_large_deals_summary(
repository,
year,
month,
ct,
scope,
scope_value,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"统计失败:{exc}") from exc
@app.post("/imports/monthly")
def import_monthly(file: UploadFile = File(...)) -> dict[str, object]:
upload_filename = Path(file.filename or "").name
if not is_allowed_upload_filename(upload_filename):
raise HTTPException(status_code=400, detail="请上传 .xlsx 格式的月更新数据文件")
with NamedTemporaryFile(prefix="market-api-upload-", suffix=".xlsx") as temporary_file:
copyfileobj(file.file, temporary_file)
temporary_file.flush()
temporary_file.seek(0)
try:
summary = import_monthly_excel(
get_settings(),
temporary_file.name,
source_filename=upload_filename,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"导入失败:{exc}") from exc
return {
"源文件名": summary.source_filename,
"数据库模式": summary.schema,
"总行数": summary.total_rows,
"导入表": [
{
"工作表名称": table.sheet_name,
"数据库表名": table.table_name,
"导入行数": table.rows,
}
for table in summary.tables
],
}