493 lines
16 KiB
Python
Executable File
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
|
|
],
|
|
}
|