Files
shanxia2.0/backend/app/services/pl_quota_cert_bindings.py
T

152 lines
8.3 KiB
Python

"""证面、购买证引用与多合同范围先整批校验,再在调用方事务内差量写入。"""
from typing import List, Literal, Optional
from uuid import uuid4
from fastapi import HTTPException
from pydantic import BaseModel, Field
from app.crud.crm.data_scope_helpers import apply_crm_owner_scope
from app.models.crm_models import CrmContract
from app.models.pl_quota_models import PlContractCertLink, PlContractCertProduct, PlTransportCertPurchaseRef
from app.schemas.pl.quota_cert import InputBody
from app.schemas.pl.quota_scope import ScopeProductBody
from app.services import pl_quota_cert_service as certs
from app.services import pl_quota_contract_service as scopes
class CertificateContractBinding(InputBody):
id: Optional[str] = Field(None, max_length=64)
contract_id: str = Field(min_length=1, max_length=64)
link_status: Optional[Literal['active', 'inactive']] = None
products: List[ScopeProductBody]
remark: Optional[str] = Field(None, max_length=1000)
order_num: Optional[int] = None
class CertificateBindings(InputBody):
contract_bindings: Optional[List[CertificateContractBinding]] = None
def _target_links(db, user, cert_type, cert_id, current=False):
query = db.query(PlContractCertLink).filter_by(tenant_no=user.tenant_no,
cert_type=cert_type, cert_id=cert_id, del_flag=0)
if current:
query = query.with_for_update().populate_existing()
return query.all()
def _visible_contract_ids(db, user):
query = db.query(CrmContract.id).filter_by(tenant_no=user.tenant_no, del_flag=0)
return apply_crm_owner_scope(query, CrmContract, user.to_user_info(), db, user.tenant_no)
def get_cert(db, user, cert_type, cert_id, current=False):
# 保存已持有目标证锁;重复当前读不会取得新的前置证锁。
result = certs.get_cert(db, user, cert_type, cert_id, current=current)
def rows(query):
if current:
query = query.with_for_update().populate_existing()
return query.all()
links = rows(db.query(PlContractCertLink).filter(PlContractCertLink.tenant_no == user.tenant_no,
PlContractCertLink.cert_type == cert_type, PlContractCertLink.cert_id == cert_id,
PlContractCertLink.del_flag == 0,
PlContractCertLink.contract_id.in_(_visible_contract_ids(db, user))).order_by(
PlContractCertLink.create_time.desc(), PlContractCertLink.id))
products = rows(db.query(PlContractCertProduct).filter(
PlContractCertProduct.tenant_no == user.tenant_no, PlContractCertProduct.del_flag == 0,
PlContractCertProduct.contract_cert_link_id.in_([r.id for r in links])).order_by(
PlContractCertProduct.create_time.desc(), PlContractCertProduct.id)) if links else []
# 直接读取ID,避免 count() 派生表把锁读退化为一致性快照。
total_links = len(rows(db.query(PlContractCertLink.id).filter_by(tenant_no=user.tenant_no,
cert_type=cert_type, cert_id=cert_id, del_flag=0)))
result['bindingsEditable'] = total_links == len(links)
result['contractBindings'] = [{
'id': link.id, 'contractId': link.contract_id, 'linkStatus': link.link_status,
'remark': link.remark, 'orderNum': link.order_num,
'products': [{'id': p.id, 'contractProductId': p.contract_product_id,
'scopeStatus': p.scope_status, 'remark': p.remark, 'orderNum': p.order_num}
for p in products if p.contract_cert_link_id == link.id],
} for link in links]
return result
def _reference_ids(db, user, cert_id, submitted, current=False):
query = db.query(PlTransportCertPurchaseRef).filter_by(tenant_no=user.tenant_no,
transport_cert_id=cert_id)
if current:
query = query.with_for_update().populate_existing()
restored = {r['reference_no']: r for r in (submitted or [])}
ids = {r.purchase_cert_id for r in query.all() if r.purchase_cert_id and (
not r.del_flag or (r.reference_no in restored and 'purchase_cert_id' not in restored[r.reference_no]))}
ids.update(r['purchase_cert_id'] for r in (submitted or []) if r.get('purchase_cert_id'))
return ids
def save_cert(db, user, cert_type, data, cert_id=None):
payload = data.model_dump(exclude_unset=True) if isinstance(data, BaseModel) else dict(data)
present = 'contractBindings' in payload or 'contract_bindings' in payload
if not present:
result = certs.save_cert(db, user, cert_type, payload, cert_id)
return get_cert(db, user, cert_type, result['id'], current=True)
bindings_payload = {key: payload.pop(key) for key in ('contractBindings', 'contract_bindings') if key in payload}
body = CertificateBindings(**bindings_payload)
if body.contract_bindings is None:
raise HTTPException(400, '合同集合不可为null;清空请提交[]')
# 证面独立严格解析,未识别字段不会因拆分而被丢弃。
certs._model(cert_type)
face = certs.SCHEMAS[cert_type][1 if cert_id else 0](**payload)
values = body.model_dump(exclude_unset=True)['contract_bindings']
ids = [r['contract_id'] for r in values]
if len(ids) != len(set(ids)):
raise HTTPException(400, '合同重复提交')
effective_id = cert_id or str(uuid4())
target = (cert_type, effective_id)
initial = _target_links(db, user, *target)
contract_ids = set(ids) | {r.contract_id for r in initial}
# 无权合同即拒绝整个集合替换;省略集合仍允许修改证面。
for cid in sorted(contract_ids):
scopes._contract(db, user, cid)
keys = {target}
for cid in sorted(contract_ids):
keys.update((r.cert_type, r.cert_id) for r in scopes._links(db, user, cid) if r.del_flag == 0)
refs = face.model_dump(exclude_unset=True).get('purchase_refs')
if cert_type == 'transport':
keys.update(('purchase', pid) for pid in _reference_ids(db, user, effective_id, refs))
locked = {key: certs._get_row(db, user, *key, lock=True) for key in sorted(keys)
if cert_id or key != target}
# 新证仅构建临时对象,不 add/flush;全部准备成功后才能入会话。
if not cert_id:
locked[target] = certs.MODELS[cert_type](id=effective_id, status=face.status or 'draft')
contracts = {cid: scopes._contract(db, user, cid, lock=True) for cid in sorted(contract_ids)}
current = _target_links(db, user, *target, current=True)
if any(r.contract_id not in contract_ids for r in current):
raise HTTPException(409, '证件合同范围已变更,请重试')
if cert_type == 'transport' and any(('purchase', pid) not in keys for pid in
_reference_ids(db, user, effective_id, refs, current=True)):
raise HTTPException(409, '证面引用已变更,请重试')
prepared_face = certs.prepare_cert(db, user, cert_type, face, cert_id,
locked=locked, effective_id=effective_id)
# 用最终证面状态校验作废证不可扩大,不修改持久对象。
effective_certs = dict(locked)
effective_certs[target] = certs.MODELS[cert_type](id=effective_id,
status=prepared_face['values'].get('status', locked[target].status))
requested = {item['contract_id']: item for item in values}
prepared = []
for cid in sorted(contract_ids):
existing = scopes._links(db, user, cid, lock=True)
if any((r.cert_type, r.cert_id) not in keys for r in existing if r.del_flag == 0):
raise HTTPException(409, '合同用证范围已变更,请重试')
entries = [{'id': r.id, 'cert_type': r.cert_type, 'cert_id': r.cert_id}
for r in existing if r.del_flag == 0 and (r.cert_type, r.cert_id) != target]
if cid in requested:
item = {k: v for k, v in requested[cid].items() if k != 'contract_id'}
item.update(cert_type=cert_type, cert_id=effective_id)
entries.append(item)
normalized = scopes.ContractCertBindingsBody(bindings=entries).model_dump(exclude_unset=True)['bindings']
prepared.append(scopes.prepare_contract_bindings(db, user, contracts[cid], normalized,
effective_certs, keys))
row = certs.apply_cert(db, user, prepared_face)
for scope in prepared:
scopes.apply_contract_bindings(db, user, scope)
db.flush()
return get_cert(db, user, cert_type, row.id, current=True)