Files
group_xinghuo_jinrong/app/main.py
T
zhanghongyu_0626 2b4eaa9725 feat(nav): Implement historical NAV tracking and enhance product API
- Added `list_nav_history` method in `CoreReadOnlyRepository` to retrieve historical NAV data for products over a specified number of days.
- Introduced `get_nav_history` endpoint in `product_service` to return historical NAV sequences, improving product data accessibility.
- Updated `products.py` to include new API endpoint for fetching NAV history, ensuring compliance with access control.
- Enhanced `ready.py` to include a health check for the new functionality, ensuring system readiness.
- Updated documentation to reflect the new testing baseline of 833 passed tests, indicating improved stability and functionality across the application.

This update significantly enhances the product API by providing access to historical NAV data, improving user insights into product performance.
2026-09-11 20:47:18 +08:00

166 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""FastAPI 入口:宿主 Wave 0(auth/chat/TraceMiddleware)+ 风控模块 router/中间件/lifespan。"""
from __future__ import annotations
import logging
import re
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
from app.api import deps
from app.api.audit_middleware import audit_middleware
from app.api.auth import router as auth_router
from app.api.advisors import router as advisors_router
from app.api.chat import router as chat_router
from app.api.compliance import router as compliance_router
from app.api.customers import router as customers_router
from app.api.products import router as products_router
from app.api.risk import router as risk_router
from app.api.simulate import router as simulate_router
from app.api.staff import router as staff_router
from app.api.analyst import router as analyst_router
from app.api.ready import build_ready_payload, router as ready_router
from app.api.visitor import router as visitor_router
from app.config.settings import settings
from app.middleware.trace import TraceMiddleware
from app.repository.audit_repository import AuditRepository
from app.service.auth_service import jwt_ready
from app.service.risk import redis_gateway
from app.utils.db import dispose_engines
from app.utils.exceptions import AppError
from app.utils.exceptions import ApiError, AppError, PermissionDenied
from app.utils.response import error_body, fail, register_error_handlers, trace_headers
from app.utils.trace import (
bind_request_id,
new_trace,
reset_request_id,
reset_trace,
set_trace,
)
logger = logging.getLogger(__name__)
_TRACE_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
# FastAPI 已注册 handler 的异常须原样冒泡;BaseHTTPMiddleware 对未捕获异常会 re-raise。
_HANDLED_APP_EXCEPTIONS = (
ApiError,
AppError,
PermissionDenied,
RequestValidationError,
StarletteHTTPException,
)
@asynccontextmanager
async def lifespan(_: FastAPI):
if settings.app_env != "development":
if deps.AUTH_FACTORY_IS_DEBUG:
raise RuntimeError(
"debug auth factory is wired but app_env is not 'development'; "
"deploy T-01 JWT auth first or set app_env=development"
)
reason = jwt_ready()
if reason:
raise RuntimeError(f"JWT auth not ready for non-development env: {reason}")
if settings.app_env == "development" and settings.jwt_dev_secret == "change-me-in-dev-only":
logger.warning(
"JWT_DEV_SECRET is the public default; issued dev tokens are forgeable "
"(demo/CI only, never expose to untrusted networks)"
)
redis_gateway.set_gateway(redis_gateway.RedisGateway())
try:
yield
finally:
redis_gateway.set_gateway(None)
dispose_engines()
app = FastAPI(title="JinRong Agent Platform", version="0.2.0", lifespan=lifespan)
register_error_handlers(app)
app.add_middleware(TraceMiddleware)
app.include_router(auth_router)
app.include_router(customers_router)
app.include_router(products_router)
app.include_router(advisors_router)
app.include_router(staff_router)
app.include_router(compliance_router)
app.include_router(chat_router)
app.include_router(visitor_router)
app.include_router(risk_router)
app.include_router(simulate_router)
app.include_router(analyst_router)
app.include_router(ready_router)
@app.get("/api/ready", tags=["platform"], include_in_schema=True)
def api_ready_probe(request: Request):
"""与 ready_router 同逻辑;在 app 上再挂一份,避免旧进程/路由表遗漏时 404。"""
trace_id = getattr(request.state, "trace_id", "unknown")
return build_ready_payload(app, trace_id)
@app.middleware("http")
async def audit_middleware_entry(request: Request, call_next):
return await audit_middleware(request, call_next)
@app.middleware("http")
async def trace_middleware(request: Request, call_next):
incoming = request.headers.get("X-Trace-Id", "")
trace_id = incoming if _TRACE_ID_PATTERN.fullmatch(incoming) else new_trace()
request.state.trace_id = trace_id
token = set_trace(trace_id)
request_id, rid_token = bind_request_id(request.headers.get("X-Request-Id", ""))
request.state.request_id = request_id
try:
try:
response = await call_next(request)
except _HANDLED_APP_EXCEPTIONS:
raise
except Exception:
logger.exception("unhandled error on %s %s", request.method, request.url.path)
response = JSONResponse(
status_code=500,
content=error_body("INTERNAL_ERROR", "internal server error"),
headers=trace_headers(request),
)
finally:
reset_trace(token)
reset_request_id(rid_token)
response.headers["X-Trace-Id"] = trace_id
response.headers["X-Request-Id"] = request_id
return response
@app.get("/health")
def health(request: Request, probe: bool = False):
trace_id = getattr(request.state, "trace_id", "unknown")
if probe:
return build_ready_payload(app, trace_id)
return {"status": "ok", "env": settings.app_env, "trace_id": trace_id}
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
trace_id = getattr(request.state, "trace_id", "unknown")
if exc.audit_event:
agent_type = request.headers.get("X-Agent-Type", "platform")
try:
AuditRepository().insert(
trace_id=trace_id,
event_type=exc.audit_event,
agent_type=agent_type if agent_type in ("customer", "advisor", "analyst", "risk") else "platform",
actor_id="anonymous",
decision=exc.error_code,
input_summary={"message": exc.message},
)
except Exception:
pass
body = fail(exc.code, exc.message, trace_id, data={"error_code": exc.error_code})
return JSONResponse(status_code=exc.http_status, content=body.model_dump())