修复:限定幂等键精确业务域

This commit is contained in:
焦龙言
2026-07-27 18:54:50 +08:00
parent 4a321098f8
commit b6f3b1e1f0
18 changed files with 730 additions and 135 deletions
@@ -431,15 +431,16 @@ class OperationDao:
cls,
db: AsyncSession,
idempotency_key: str,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
source_site_id: int,
target_warehouse_id: int,
) -> BizReturnOrder | None:
return await cls._get_by_key(
db,
BizReturnOrder,
BizReturnOrder.request_idempotency_key,
idempotency_key,
*cls._return_scope_conditions(site_ids, warehouse_ids),
BizReturnOrder.source_site_id == source_site_id,
BizReturnOrder.target_warehouse_id == target_warehouse_id,
)
@classmethod
@@ -447,15 +448,16 @@ class OperationDao:
cls,
db: AsyncSession,
idempotency_key: str,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
source_site_id: int,
target_warehouse_id: int,
) -> BizReturnOrder | None:
return await cls._get_by_key(
db,
BizReturnOrder,
BizReturnOrder.confirm_idempotency_key,
idempotency_key,
*cls._return_scope_conditions(site_ids, warehouse_ids),
BizReturnOrder.source_site_id == source_site_id,
BizReturnOrder.target_warehouse_id == target_warehouse_id,
)
@classmethod
@@ -12,6 +12,11 @@ class BizInboundOrder(Base):
__tablename__ = 'biz_inbound_order'
__table_args__ = (
UniqueConstraint(
'warehouse_id',
'confirm_idempotency_key',
name='uq_biz_inbound_order_warehouse_confirm_idempotency',
),
CheckConstraint("status IN ('DRAFT', 'CONFIRMED')", name='ck_biz_inbound_order_status'),
{'comment': '首次入库单'},
)
@@ -25,7 +30,7 @@ class BizInboundOrder(Base):
comment='仓库ID',
)
status = Column(String(32), nullable=False, server_default='DRAFT', comment='单据状态')
confirm_idempotency_key = Column(String(64), nullable=True, unique=True, comment='确认幂等键')
confirm_idempotency_key = Column(String(64), nullable=True, comment='确认幂等键')
confirm_request_hash = Column(String(64), nullable=True, comment='确认请求摘要')
confirmed_by = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=True, comment='确认人ID')
confirmed_at = Column(DateTime, nullable=True, comment='确认时间')
@@ -73,6 +78,11 @@ class BizFillingRecord(Base):
__tablename__ = 'biz_filling_record'
__table_args__ = (
UniqueConstraint(
'warehouse_id',
'idempotency_key',
name='uq_biz_filling_record_warehouse_idempotency',
),
UniqueConstraint('pallet_id', 'active_slot', name='uq_biz_filling_record_pallet_active_slot'),
CheckConstraint('initial_packages > 0', name='ck_biz_filling_record_initial_packages'),
CheckConstraint(
@@ -99,7 +109,7 @@ class BizFillingRecord(Base):
initial_packages = Column(Integer, nullable=False, comment='初始包数')
active_slot = Column(String(16), nullable=True, comment='活动装填槽')
closed_at = Column(DateTime, nullable=True, comment='装填周期关闭时间')
idempotency_key = Column(String(64), nullable=False, unique=True, comment='幂等键')
idempotency_key = Column(String(64), nullable=False, comment='幂等键')
request_hash = Column(String(64), nullable=False, comment='请求摘要')
operator_id = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=False, comment='操作人ID')
operated_at = Column(DateTime, nullable=False, comment='操作时间')
@@ -120,6 +130,12 @@ class BizShipmentOrder(Base):
__tablename__ = 'biz_shipment_order'
__table_args__ = (
UniqueConstraint(
'source_warehouse_id',
'target_site_id',
'confirm_idempotency_key',
name='uq_biz_shipment_order_route_confirm_idempotency',
),
CheckConstraint("status IN ('DRAFT', 'CONFIRMED')", name='ck_biz_shipment_order_status'),
{'comment': '整托发货单'},
)
@@ -139,7 +155,7 @@ class BizShipmentOrder(Base):
comment='目标现场ID',
)
status = Column(String(32), nullable=False, server_default='DRAFT', comment='单据状态')
confirm_idempotency_key = Column(String(64), nullable=True, unique=True, comment='确认幂等键')
confirm_idempotency_key = Column(String(64), nullable=True, comment='确认幂等键')
confirm_request_hash = Column(String(64), nullable=True, comment='确认请求摘要')
confirmed_by = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=True, comment='确认人ID')
confirmed_at = Column(DateTime, nullable=True, comment='确认时间')
@@ -198,6 +214,7 @@ class BizUsageRecord(Base):
__tablename__ = 'biz_usage_record'
__table_args__ = (
UniqueConstraint('site_id', 'idempotency_key', name='uq_biz_usage_record_site_idempotency'),
CheckConstraint('used_packages > 0', name='ck_biz_usage_record_used_packages'),
CheckConstraint(
'remaining_packages_after >= 0',
@@ -219,7 +236,7 @@ class BizUsageRecord(Base):
line_id = Column(BigInteger, ForeignKey('biz_production_line.line_id'), nullable=False, comment='产线ID')
used_packages = Column(Integer, nullable=False, comment='本次使用包数')
remaining_packages_after = Column(Integer, nullable=False, comment='使用后剩余包数')
idempotency_key = Column(String(64), nullable=False, unique=True, comment='幂等键')
idempotency_key = Column(String(64), nullable=False, comment='幂等键')
request_hash = Column(String(64), nullable=False, comment='请求摘要')
operator_id = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=False, comment='操作人ID')
occurred_at = Column(DateTime, nullable=False, comment='发生时间')
@@ -230,6 +247,18 @@ class BizReturnOrder(Base):
__tablename__ = 'biz_return_order'
__table_args__ = (
UniqueConstraint(
'source_site_id',
'target_warehouse_id',
'request_idempotency_key',
name='uq_biz_return_order_route_request_idempotency',
),
UniqueConstraint(
'source_site_id',
'target_warehouse_id',
'confirm_idempotency_key',
name='uq_biz_return_order_route_confirm_idempotency',
),
CheckConstraint("status IN ('REQUESTED', 'CONFIRMED')", name='ck_biz_return_order_status'),
{'comment': '托盘退回单'},
)
@@ -249,9 +278,9 @@ class BizReturnOrder(Base):
comment='目标仓库ID',
)
status = Column(String(32), nullable=False, server_default='REQUESTED', comment='退回状态')
request_idempotency_key = Column(String(64), nullable=False, unique=True, comment='申请幂等键')
request_idempotency_key = Column(String(64), nullable=False, comment='申请幂等键')
request_hash = Column(String(64), nullable=False, comment='申请请求摘要')
confirm_idempotency_key = Column(String(64), nullable=True, unique=True, comment='确认幂等键')
confirm_idempotency_key = Column(String(64), nullable=True, comment='确认幂等键')
confirm_request_hash = Column(String(64), nullable=True, comment='确认请求摘要')
requested_by = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=False, comment='申请人ID')
requested_at = Column(DateTime, nullable=False, comment='申请时间')
@@ -300,7 +300,7 @@ class FillingService:
recovered = await OperationDao.get_filling_by_idempotency_key(
query_db,
page_object.idempotency_key,
warehouse_ids=warehouse_ids,
warehouse_ids=[page_object.warehouse_id],
)
if recovered is None:
raise ServiceException(message='装填失败,数据冲突')
@@ -333,7 +333,7 @@ class FillingService:
existing = await OperationDao.get_filling_by_idempotency_key(
query_db,
page_object.idempotency_key,
warehouse_ids=warehouse_ids,
warehouse_ids=[page_object.warehouse_id],
)
if existing is not None:
await AccessService.assert_warehouse_access(
@@ -513,10 +513,17 @@ class InboundService:
) -> CrudResponseModel:
try:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
current = await OperationDao.get_inbound_by_id(
query_db,
inbound_id,
warehouse_ids=warehouse_ids,
)
if current is None:
cls._raise_inbound_not_accessible(current_user)
recovered = await OperationDao.get_inbound_by_confirm_idempotency_key(
query_db,
idempotency_key,
warehouse_ids=warehouse_ids,
warehouse_ids=[current.warehouse_id],
)
if recovered is None:
raise ServiceException(message='确认入库失败,幂等冲突')
@@ -575,7 +582,7 @@ class InboundService:
existing = await OperationDao.get_inbound_by_confirm_idempotency_key(
query_db,
idempotency_key,
warehouse_ids=warehouse_ids,
warehouse_ids=[inbound.warehouse_id],
)
if existing is not None:
await AccessService.assert_warehouse_access(
@@ -586,12 +586,14 @@ class ReturnService:
try:
site_ids = await cls._site_scope(query_db, current_user)
request_warehouse_ids = None if site_ids is None else []
recovered = await OperationDao.get_return_by_request_idempotency_key(
query_db,
page_object.idempotency_key,
site_ids=site_ids,
warehouse_ids=request_warehouse_ids,
)
recovered = None
if site_ids is None or page_object.source_site_id in site_ids:
recovered = await OperationDao.get_return_by_request_idempotency_key(
query_db,
page_object.idempotency_key,
source_site_id=page_object.source_site_id,
target_warehouse_id=page_object.target_warehouse_id,
)
if recovered is None or not cls._is_exact_request_result(recovered, page_object, request_hash):
raise ServiceException(message='申请退回失败,幂等冲突')
locked = await OperationDao.get_return_for_update(
@@ -640,8 +642,8 @@ class ReturnService:
existing = await OperationDao.get_return_by_request_idempotency_key(
query_db,
page_object.idempotency_key,
site_ids=preflight_site_ids,
warehouse_ids=request_warehouse_ids,
source_site_id=page_object.source_site_id,
target_warehouse_id=page_object.target_warehouse_id,
)
if existing is not None and not cls._is_exact_request_result(existing, page_object, request_hash):
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
@@ -681,12 +683,11 @@ class ReturnService:
fresh_site_ids = await cls._site_scope(query_db, current_user)
if fresh_site_ids is not None and page_object.source_site_id not in fresh_site_ids:
cls._raise_pallet_not_accessible(current_user)
fresh_warehouse_ids = None if fresh_site_ids is None else []
fresh_existing = await OperationDao.get_return_by_request_idempotency_key(
query_db,
page_object.idempotency_key,
site_ids=fresh_site_ids,
warehouse_ids=fresh_warehouse_ids,
source_site_id=page_object.source_site_id,
target_warehouse_id=page_object.target_warehouse_id,
)
if fresh_existing is not None:
if not cls._is_exact_request_result(fresh_existing, page_object, request_hash):
@@ -905,11 +906,19 @@ class ReturnService:
try:
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
confirm_site_ids = None if warehouse_ids is None else []
route = await OperationDao.get_return_by_id(
query_db,
return_id,
site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids,
)
if route is None:
cls._raise_return_not_accessible(current_user)
recovered = await OperationDao.get_return_by_confirm_idempotency_key(
query_db,
page_object.idempotency_key,
site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids,
source_site_id=route.source_site_id,
target_warehouse_id=route.target_warehouse_id,
)
if recovered is None:
raise ServiceException(message='确认退回失败,幂等冲突')
@@ -955,20 +964,6 @@ class ReturnService:
request_hash = cls._build_confirm_request_hash(return_id, page_object)
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
confirm_site_ids = None if warehouse_ids is None else []
existing = await OperationDao.get_return_by_confirm_idempotency_key(
query_db,
page_object.idempotency_key,
site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids,
)
if existing is not None and not cls._is_exact_confirm_result(
existing,
return_id,
page_object,
request_hash,
):
raise ServiceException(message='确认退回失败,幂等键已被其他请求使用')
return_order = await OperationDao.get_return_by_id(
query_db,
return_id,
@@ -985,6 +980,19 @@ class ReturnService:
)
except PermissionException:
cls._raise_return_not_accessible(current_user)
existing = await OperationDao.get_return_by_confirm_idempotency_key(
query_db,
page_object.idempotency_key,
source_site_id=return_order.source_site_id,
target_warehouse_id=return_order.target_warehouse_id,
)
if existing is not None and not cls._is_exact_confirm_result(
existing,
return_id,
page_object,
request_hash,
):
raise ServiceException(message='确认退回失败,幂等键已被其他请求使用')
await query_db.rollback()
locked = await OperationDao.get_return_for_update(
@@ -750,11 +750,19 @@ class ShipmentService:
) -> CrudResponseModel:
try:
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
current = await OperationDao.get_shipment_by_id(
query_db,
shipment_id,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
)
if current is None:
cls._raise_shipment_not_accessible(current_user)
recovered = await OperationDao.get_shipment_by_confirm_idempotency_key(
query_db,
idempotency_key,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
warehouse_ids=[current.source_warehouse_id],
site_ids=[current.target_site_id],
)
if recovered is None:
raise ServiceException(message='确认发货失败,幂等冲突')
@@ -815,8 +823,8 @@ class ShipmentService:
existing = await OperationDao.get_shipment_by_confirm_idempotency_key(
query_db,
idempotency_key,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
warehouse_ids=[shipment.source_warehouse_id],
site_ids=[shipment.target_site_id],
)
if existing is not None and not cls._is_exact_confirmed(
existing,
@@ -179,7 +179,7 @@ class UsageService:
recovered = await OperationDao.get_usage_by_idempotency_key(
query_db,
page_object.idempotency_key,
site_ids=site_ids,
site_ids=[expected_site_id],
)
if recovered is None:
raise ServiceException(message='使用失败,数据冲突')
@@ -351,11 +351,18 @@ class UsageService:
request_hash = cls._build_request_hash(page_object)
user_id, user_name = cls._current_identity(current_user)
preflight_site_ids = await cls._site_scope(query_db, current_user)
existing = await OperationDao.get_usage_by_idempotency_key(
query_db,
page_object.idempotency_key,
site_ids=preflight_site_ids,
)
# 产线归属是请求内稳定路由;锁事务仍以库存中的实际现场复核。
preflight_line = await MasterDao.get_line(query_db, page_object.line_id)
preflight_site_id = preflight_line.site_id if preflight_line is not None else None
existing = None
if preflight_site_id is not None and (
preflight_site_ids is None or preflight_site_id in preflight_site_ids
):
existing = await OperationDao.get_usage_by_idempotency_key(
query_db,
page_object.idempotency_key,
site_ids=[preflight_site_id],
)
if existing is not None:
if not cls._is_exact_idempotent_result(existing, page_object, request_hash):
raise ServiceException(message='使用失败,幂等键已被其他请求使用')
@@ -390,6 +397,8 @@ class UsageService:
expected_site_id = inventory.site_id
await cls._assert_pallet_site_access(query_db, current_user, inventory.site_id)
locked_site_ids = await cls._site_scope(query_db, current_user)
if locked_site_ids is not None and inventory.site_id not in locked_site_ids:
cls._raise_pallet_not_accessible(current_user)
locked_pallets = await InventoryDao.get_pallets_by_ids(
query_db,
@@ -410,7 +419,7 @@ class UsageService:
locked_existing = await OperationDao.get_usage_by_idempotency_key(
query_db,
page_object.idempotency_key,
site_ids=locked_site_ids,
site_ids=[inventory.site_id],
)
if locked_existing is not None:
if locked_existing.site_id != inventory.site_id or not cls._is_exact_idempotent_result(