fix(pl): Read current certificate bindings when returning atomic saves.

This commit is contained in:
wuzilong committed 2026-11-01 21:26:46 +08:00
1 parent 71116ed69e
commit 5912e9d056
2 files changed
+69 -11

No files matched your search

+16 -10
View File
@@ -39,19 +39,25 @@ def _visible_contract_ids(db, user):
return apply_crm_owner_scope(query, CrmContract, user.to_user_info(), db, user.tenant_no)
def get_cert(db, user, cert_type, cert_id):
result = certs.get_cert(db, user, cert_type, cert_id)
links = db.query(PlContractCertLink).filter(PlContractCertLink.tenant_no == 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).all()
products = db.query(PlContractCertProduct).filter(
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).all() if links else []
total_links = db.query(PlContractCertLink.id).filter_by(tenant_no=user.tenant_no,
cert_type=cert_type, cert_id=cert_id, del_flag=0).count()
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,
@@ -80,7 +86,7 @@ def save_cert(db, user, cert_type, data, cert_id=None):
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'])
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:
@@ -142,4 +148,4 @@ def save_cert(db, user, cert_type, data, cert_id=None):
for scope in prepared:
scopes.apply_contract_bindings(db, user, scope)
db.flush()
return get_cert(db, user, cert_type, row.id)
return get_cert(db, user, cert_type, row.id, current=True)
+53 -1
View File
@@ -115,7 +115,8 @@ def test_transport_locks_all_certificates_before_contracts_and_current_reads(cas
finally:
event.remove(c.db, 'do_orm_execute', capture)
assert events == [('cert', 'purchase', 'g1'), ('cert', 'purchase', 'g2'),
('cert', 'transport', target), ('contract', 'c1'), ('contract', 'c2')]
('cert', 'transport', target), ('contract', 'c1'), ('contract', 'c2'),
('cert', 'transport', target)] # 响应当前读仅重复已持有的目标证锁。
for model in (CrmContract, CrmContractProduct, c.m.PlContractCertLink,
c.m.PlTransportCertPurchaseRef):
assert any(m == model and refresh for m, refresh in reads)
@@ -201,3 +202,54 @@ def test_transport_refs_and_face_not_written_when_second_contract_invalid(case):
assert c.db.query(c.m.PlTransportCertPurchaseRef).count() == 0
assert c.db.query(c.m.PlContractCertLink).count() == 0
assert not c.db.dirty and not c.db.new
@pytest.mark.parametrize('with_bindings', [True, False])
def test_save_response_uses_current_reads_for_face_bindings_products_and_visibility(case, monkeypatch, with_bindings):
"""SQLite 不模拟 RR;捕获实际 ORM 返回查询并以 MySQL 方言验证当前读意图。"""
from sqlalchemy import event
from sqlalchemy.dialects import mysql
from app.services import pl_quota_cert_bindings as svc
c = case
save(c, {'contractBindings': scopes()})
reads = []
returning = [False]
original = svc.get_cert
def response(*args, **kwargs):
returning[0] = True
try:
return original(*args, **kwargs)
finally:
returning[0] = False
def capture(state):
if returning[0] and state.is_select:
reads.append((state.bind_mapper.class_, str(state.statement.compile(dialect=mysql.dialect())),
state.load_options._populate_existing))
monkeypatch.setattr(svc, 'get_cert', response)
event.listen(c.db, 'do_orm_execute', capture)
try:
result = save(c, {'contractBindings': scopes()} if with_bindings else {'remark': 'face only'})
finally:
event.remove(c.db, 'do_orm_execute', capture)
assert len(result['contractBindings']) == 2
for model in (c.m.PlPurchaseCert, c.m.PlContractCertLink, c.m.PlContractCertProduct):
queries = [(sql, refresh) for m, sql, refresh in reads if m == model]
assert queries, model.__name__
assert all('FOR UPDATE' in sql and refresh for sql, refresh in queries), queries
def test_plain_get_does_not_acquire_write_locks(case):
from sqlalchemy import event
from app.services import pl_quota_cert_bindings as svc
c = case
save(c, {'contractBindings': scopes()})
reads = []
def capture(state):
if state.is_select:
reads.append(state.statement._for_update_arg is not None)
event.listen(c.db, 'do_orm_execute', capture)
try:
result = svc.get_cert(c.db, c.user, 'purchase', 'g1')
finally:
event.remove(c.db, 'do_orm_execute', capture)
assert len(result['contractBindings']) == 2 and not any(reads)