"""共享证件档案、合同产品适用范围与合法使用证明入口。""" from dataclasses import replace from datetime import date from decimal import Decimal from typing import Literal, Optional from fastapi import APIRouter, Body, Depends, HTTPException, Query from fastapi.encoders import jsonable_encoder from pydantic import ValidationError, Field from sqlalchemy.orm import Session from app.core.database import get_db from app.core.db_auth import DbUserContext, get_db_current_user from app.core.menu_guard import require_menu_code from app.core.oper_log_decorator import oper_log from app.core.tenant_context import get_tenant_no from app.schemas.pl.quota_cert import InputBody, CertConfirmBody, CertVerifyBody, CertTotalAdjustBody from app.schemas.pl.quota_scope import ContractCertBindingsBody from app.schemas.pl.legal_use_document import LegalDocumentsBody, StopLegalDocumentsBody from app.schemas.pl.quota_planning import PlanQuotaConfiguration, PlanQuotaPreview from app.services import pl_quota_cert_service as certs from app.services import pl_quota_cert_bindings as cert_bindings from app.services import pl_quota_contract_service as scopes from app.services import pl_quota_contract_query as contract_query from app.services import pl_legal_use_document as documents from app.services import pl_stop_legal_document as stop_documents from app.services import pl_quota_planning as planning from app.services import pl_quota_adjustment as adjustment from app.services import pl_quota_overview as overview from app.services import pl_quota_statistics as statistics from app.services.pl_quota_allocation import AllocationIncomplete, InsufficientQuota from app.services.pl_quota_attachments import read_image from app.services.pl_quota_recognition import recognize_quota_cert router = APIRouter(prefix='/transport/quota-certs', tags=['购买证与运输证'], dependencies=[Depends(require_menu_code('pl:quota-cert:list'))]) legal_router = APIRouter(prefix='/transport/legal-use-documents', tags=['合法使用证明'], dependencies=[Depends(require_menu_code('pl:legal-use-document:list'))]) CertType = Literal['purchase', 'transport'] def current_user(db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user)): return replace(user, tenant_no=get_tenant_no(db, user.to_tenant_user_dict())) def _wire(value): return jsonable_encoder(value, custom_encoder={Decimal: str}) def _write(db, action): try: result = action() db.commit() return _wire(result) except ValidationError as error: db.rollback() # 不回显输入值,避免识别或附件请求中的敏感信息进入错误日志。 raise HTTPException(422, [{'loc': e['loc'], 'msg': e['msg'], 'type': e['type']} for e in error.errors()]) from None except Exception: db.rollback() raise class RecognizeBody(InputBody): attachment_url: str = Field(min_length=1, max_length=2000) @router.get('/plans') def plan_overview(keyword: str = '', status: Optional[str] = None, page: int = Query(1, ge=1), page_size: int = Query(20, alias='pageSize', ge=1, le=100), plan_no: Optional[str] = Query(None, alias='planNo', max_length=200), license_plate: Optional[str] = Query(None, alias='licensePlate', max_length=100), plan_date_from: Optional[date] = Query(None, alias='planDateFrom'), plan_date_to: Optional[date] = Query(None, alias='planDateTo'), quota_status_in: Optional[str] = Query(None, alias='quotaStatusIn', max_length=200), db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(overview.list_plans(db, user, keyword, status, page, page_size, plan_no=plan_no, license_plate=license_plate, plan_date_from=plan_date_from, plan_date_to=plan_date_to, quota_status_in=quota_status_in)) @router.get('/plans/{plan_id}/usage') def plan_quota(plan_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(planning.get_plan_quota(db, user, plan_id)) def _preview_body(body): values = body.model_dump(exclude={'target_license_plate', 'target_trailer_no'}) vehicle = {} if 'target_license_plate' in body.model_fields_set: vehicle['licensePlate'] = body.target_license_plate if 'target_trailer_no' in body.model_fields_set: vehicle['trailerNo'] = body.target_trailer_no return PlanQuotaConfiguration.model_validate(values), vehicle or None @router.post('/plans/{plan_id}/context') def plan_configuration_context(plan_id: str, body: PlanQuotaPreview, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): config, vehicle = _preview_body(body) return _wire(planning.configuration_context(db, user, plan_id, config, vehicle)) @router.post('/plans/{plan_id}/preview') def preview_plan(plan_id: str, body: PlanQuotaPreview, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): try: config, vehicle = _preview_body(body) return _wire(planning.preview_plan(db, user, plan_id, config, vehicle)) except AllocationIncomplete: return {'status': 'calculation_incomplete', 'reasons': ['分配计算尚未完成,请缩小候选范围或调整后重试']} except InsufficientQuota as error: return {'status': 'insufficient', 'reasons': [str(error)], 'diagnostics': getattr(error, 'diagnostics', [])} except ValueError as error: return {'status': 'needs_configuration', 'reasons': [str(error)]} @router.put('/plans/{plan_id}/configuration', dependencies=[Depends(require_menu_code('pl:quota-cert:edit'))]) @oper_log('计划用证配置', business_type=2, path_param='plan_id') def save_plan_configuration(plan_id: str, body: PlanQuotaConfiguration, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _write(db, lambda: planning.save_configuration(db, user, plan_id, body)) @router.get('/contracts') def contract_options(keyword: Optional[str] = None, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return contract_query.contract_options(db, user, keyword) @router.get('/contracts/{contract_id}/products') def contract_products(contract_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(contract_query.contract_products(db, user, contract_id)) @router.get('/contracts/{contract_id}/bindings') def get_bindings(contract_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(scopes.get_contract_bindings(db, user, contract_id)) @router.put('/contracts/{contract_id}/bindings', dependencies=[Depends(require_menu_code('pl:quota-cert:edit'))]) @oper_log('合同适用证件', business_type=2, path_param='contract_id') def save_bindings(contract_id: str, body: ContractCertBindingsBody, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _write(db, lambda: scopes.save_contract_bindings(db, user, contract_id, body)) @router.get('/statistics/contracts') def contract_statistics_list(keyword: str = Query('', max_length=200), contract_no: str = Query('', alias='contractNo', max_length=200), page: int = Query(1, ge=1), page_size: int = Query(20, alias='pageSize', ge=1, le=100), db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(statistics.list_contracts(db, user, keyword=keyword, page=page, page_size=page_size, contract_no=contract_no)) @router.get('/statistics/certificates') def certificate_statistics_list(keyword: str = Query('', max_length=200), cert_no: str = Query('', alias='certNo', max_length=200), cert_type: Optional[CertType] = Query(None, alias='certType'), page: int = Query(1, ge=1), page_size: int = Query(20, alias='pageSize', ge=1, le=100), db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(statistics.list_certificates(db, user, keyword=keyword, cert_type=cert_type, page=page, page_size=page_size, cert_no=cert_no)) @router.get('/contracts/{contract_id}/statistics') def contract_statistics(contract_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(statistics.contract_statistics(db, user, contract_id)) @router.get('/{cert_type}/{cert_id}/statistics') def certificate_statistics(cert_type: CertType, cert_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(statistics.certificate_statistics(db, user, cert_type, cert_id)) @router.get('/{cert_type}') def list_certs(cert_type: CertType, keyword: Optional[str] = None, status: Optional[str] = None, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(certs.list_certs(db, user, cert_type, keyword, status)) @router.post('/{cert_type}/recognize', dependencies=[Depends(require_menu_code('pl:quota-cert:edit'))]) @oper_log('解析证件图片') def recognize(cert_type: CertType, body: RecognizeBody, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(recognize_quota_cert(cert_type, read_image(db, user, body.attachment_url))) @router.post('/{cert_type}', dependencies=[Depends(require_menu_code('pl:quota-cert:edit'))]) @oper_log('证件档案', business_type=1) def create_cert(cert_type: CertType, body: dict = Body(...), db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _write(db, lambda: cert_bindings.save_cert(db, user, cert_type, body)) @router.get('/{cert_type}/{cert_id}') def get_cert(cert_type: CertType, cert_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(cert_bindings.get_cert(db, user, cert_type, cert_id)) @router.get('/{cert_type}/{cert_id}/contracts') def certificate_contracts(cert_type: CertType, cert_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return contract_query.certificate_contracts(db, user, cert_type, cert_id) @router.put('/{cert_type}/{cert_id}', dependencies=[Depends(require_menu_code('pl:quota-cert:edit'))]) @oper_log('证件档案', business_type=2, path_param='cert_id') def update_cert(cert_type: CertType, cert_id: str, body: dict = Body(...), db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _write(db, lambda: cert_bindings.save_cert(db, user, cert_type, body, cert_id)) @router.delete('/{cert_type}/{cert_id}', dependencies=[Depends(require_menu_code('pl:quota-cert:edit'))]) @oper_log('证件档案', business_type=3, path_param='cert_id') def delete_cert(cert_type: CertType, cert_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): def action(): certs.delete_cert(db, user, cert_type, cert_id) return {'deleted': True} return _write(db, action) @router.post('/{cert_type}/{cert_id}/confirm', dependencies=[Depends(require_menu_code('pl:quota-cert:edit'))]) @oper_log('证件期初确认', business_type=2, path_param='cert_id') def confirm_cert(cert_type: CertType, cert_id: str, body: CertConfirmBody, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _write(db, lambda: certs.confirm_cert(db, user, cert_type, cert_id, body)) @router.post('/{cert_type}/{cert_id}/verify', dependencies=[Depends(require_menu_code('pl:quota-cert:edit'))]) @oper_log('证件复核', business_type=2, path_param='cert_id') def verify_cert(cert_type: CertType, cert_id: str, body: CertVerifyBody, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _write(db, lambda: certs.verify_cert(db, user, cert_type, cert_id, body)) @router.get('/{cert_type}/{cert_id}/logs') def cert_logs(cert_type: CertType, cert_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(adjustment.list_logs(db, user, cert_type, cert_id)) @router.post('/{cert_type}/{cert_id}/total-adjust', dependencies=[Depends(require_menu_code('pl:quota-cert:edit'))]) @oper_log('证件核定总量更正', business_type=2, path_param='cert_id') def adjust_cert_total(cert_type: CertType, cert_id: str, body: CertTotalAdjustBody, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _write(db, lambda: adjustment.adjust_total(db, user, cert_type, cert_id, body)) @legal_router.get('') def list_documents(keyword: Optional[str] = None, status: Optional[str] = None, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return documents.list_documents(db, user, keyword, status) @legal_router.get('/stops') def list_stop_documents(keyword: Optional[str] = None, status: Optional[str] = None, contract_id: Optional[str] = Query(None, alias='contractId'), page: int = Query(1, ge=1), page_size: int = Query(20, alias='pageSize', ge=1, le=100), db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(stop_documents.list_documents(db, user, keyword, status, contract_id, page, page_size)) @legal_router.get('/stops/{plan_id}') def get_stop_documents(plan_id: str, contract_id: Optional[str] = Query(None, alias='contractId'), db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(stop_documents.get_documents(db, user, plan_id, contract_id)) @legal_router.patch('/stops/{plan_id}') @oper_log('逐段合法使用证明', business_type=2, path_param='plan_id') def save_stop_documents(plan_id: str, body: StopLegalDocumentsBody, contract_id: Optional[str] = Query(None, alias='contractId'), db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _write(db, lambda: stop_documents.save_documents(db, user, plan_id, body, contract_id)) @legal_router.get('/{plan_id}') def get_documents(plan_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return documents.get_documents(db, user, plan_id) @legal_router.patch('/{plan_id}') @oper_log('合法使用证明', business_type=2, path_param='plan_id') def save_documents(plan_id: str, body: LegalDocumentsBody, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _write(db, lambda: documents.save_documents(db, user, plan_id, body))