262 lines
13 KiB
Python
262 lines
13 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.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('/{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))
|