Files
shanxia2.0/backend/app/api/pl/quota_cert.py
T

295 lines
15 KiB
Python

"""共享证件档案、合同产品适用范围与合法使用证明入口。"""
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))