fix(business): 唯一键冲突重试改为审计上下文内循环,单条审计无递归
三处 create(业务/发货/回款)的 return _retry_create 是吞异常后正常 退出:外层审计把已回滚的首次尝试记成 success(假成功),重入整个 create_* 违反 audit_service「不支持嵌套」约定,且 _NO_RETRY 死常量 导致持续冲突时无界递归至 RecursionError。改为 audit 内 for 循环 (上限 _NO_RETRY),rollback 后重新锁行与取号,成功/最终失败各恰好 一条审计。新增集成用例锁定两条路径的日志增量。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
cb977979da
commit
5a3778a424
4 files changed
+290
-139
No files matched your search
@@ -55,6 +55,9 @@ from app.services.no_generator import next_no
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 编号唯一键冲突后的重试次数(模式同 business_service._NO_RETRY)
|
||||
_NO_RETRY = 1
|
||||
|
||||
|
||||
def _to_out(row: tuple) -> ReceiptOut:
|
||||
"""(receipt, business_no, shipment_no, ship_date) → 响应模型。
|
||||
@@ -84,9 +87,7 @@ def _to_out(row: tuple) -> ReceiptOut:
|
||||
|
||||
def _generate_no(session: Session) -> str:
|
||||
"""生成回款批次号(HK 前缀;MAX+FOR UPDATE 与 1062 兜底见 no_generator)。"""
|
||||
return next_no(
|
||||
session, model=Receipt, column_name="receipt_batch_no", prefix=NO_PREFIX_RECEIPT
|
||||
)
|
||||
return next_no(session, model=Receipt, column_name="receipt_batch_no", prefix=NO_PREFIX_RECEIPT)
|
||||
|
||||
|
||||
def _ensure_shipment_of_business(
|
||||
@@ -198,59 +199,55 @@ def create_receipt(
|
||||
),
|
||||
meta=meta,
|
||||
):
|
||||
if business is None:
|
||||
raise NotFoundError("业务不存在")
|
||||
if business.status != BUSINESS_STATUS_ACTIVE:
|
||||
raise BadRequestError("业务已完成或已终止,禁止新增回款")
|
||||
shipment_no, ship_date = _ensure_shipment_of_business(
|
||||
session, payload.shipment_id, payload.business_id
|
||||
)
|
||||
|
||||
try:
|
||||
receipt = receipt_repository.create(
|
||||
session,
|
||||
Receipt(
|
||||
receipt_batch_no=batch_no,
|
||||
business_id=payload.business_id,
|
||||
shipment_id=payload.shipment_id,
|
||||
receipt_date=payload.receipt_date,
|
||||
amount=Decimal(payload.amount),
|
||||
is_barter=payload.is_barter,
|
||||
settlement_status=SETTLEMENT_PENDING,
|
||||
remark=payload.remark,
|
||||
),
|
||||
)
|
||||
_recalc_totals(session, business)
|
||||
out = _to_out((receipt, business.business_no, shipment_no, ship_date))
|
||||
new_id = receipt.id
|
||||
session.commit()
|
||||
except IntegrityError as exc:
|
||||
session.rollback()
|
||||
if _is_duplicate_entry(exc):
|
||||
# 批次号撞唯一键的残余竞态:兜底重试一次(防无限重试掩盖死锁)
|
||||
return _retry_create(session, payload, operator=operator, meta=meta)
|
||||
raise
|
||||
# 批次号唯一键冲突的重试整体收在审计上下文内(模式同
|
||||
# business_service.create_business):rollback 释放旧锁,重试前重新
|
||||
# 锁业务行与生成批次号——成功/最终失败各只记一条审计,无递归
|
||||
for attempt in range(_NO_RETRY + 1):
|
||||
try:
|
||||
if business is None:
|
||||
raise NotFoundError("业务不存在")
|
||||
if business.status != BUSINESS_STATUS_ACTIVE:
|
||||
raise BadRequestError("业务已完成或已终止,禁止新增回款")
|
||||
shipment_no, ship_date = _ensure_shipment_of_business(
|
||||
session, payload.shipment_id, payload.business_id
|
||||
)
|
||||
receipt = receipt_repository.create(
|
||||
session,
|
||||
Receipt(
|
||||
receipt_batch_no=batch_no,
|
||||
business_id=payload.business_id,
|
||||
shipment_id=payload.shipment_id,
|
||||
receipt_date=payload.receipt_date,
|
||||
amount=Decimal(payload.amount),
|
||||
is_barter=payload.is_barter,
|
||||
settlement_status=SETTLEMENT_PENDING,
|
||||
remark=payload.remark,
|
||||
),
|
||||
)
|
||||
_recalc_totals(session, business)
|
||||
out = _to_out((receipt, business.business_no, shipment_no, ship_date))
|
||||
new_id, new_no = receipt.id, batch_no # 固化:commit 后不碰 ORM
|
||||
session.commit()
|
||||
break
|
||||
except IntegrityError as exc:
|
||||
session.rollback()
|
||||
if _is_duplicate_entry(exc) and attempt < _NO_RETRY:
|
||||
business = business_repository.get_by_id(
|
||||
session, payload.business_id, for_update=True
|
||||
)
|
||||
batch_no = _generate_no(session)
|
||||
continue
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
"新增回款 | receipt_id=%s | no=%s | business_id=%s",
|
||||
new_id,
|
||||
batch_no,
|
||||
new_no,
|
||||
payload.business_id,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _retry_create(
|
||||
session: Session,
|
||||
payload: ReceiptCreateIn,
|
||||
*,
|
||||
operator,
|
||||
meta: RequestMeta | None,
|
||||
) -> ReceiptOut:
|
||||
"""批次号撞唯一键后的单次重试(重新锁行与生成编号,防死锁类缺陷掩盖)。"""
|
||||
return create_receipt(session, payload, operator=operator, meta=meta)
|
||||
|
||||
|
||||
def update_receipt(
|
||||
session: Session,
|
||||
receipt_id: int,
|
||||
|
||||
Reference in new issue
Block a user