修复:强化库存事务并发与日期边界
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from module_business.entity.do.master_do import BizPallet
|
||||
@@ -15,7 +15,10 @@ class InventoryDao:
|
||||
pallet_id: int,
|
||||
) -> BizPalletInventory | None:
|
||||
result = await db.execute(
|
||||
select(BizPalletInventory).where(BizPalletInventory.pallet_id == pallet_id).with_for_update()
|
||||
select(BizPalletInventory)
|
||||
.where(BizPalletInventory.pallet_id == pallet_id)
|
||||
.with_for_update()
|
||||
.execution_options(populate_existing=True)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@@ -34,6 +37,7 @@ class InventoryDao:
|
||||
.where(BizPalletInventory.pallet_id.in_(ordered_ids))
|
||||
.order_by(BizPalletInventory.pallet_id)
|
||||
.with_for_update()
|
||||
.execution_options(populate_existing=True)
|
||||
)
|
||||
return sorted(result.scalars().all(), key=lambda inventory: inventory.pallet_id)
|
||||
|
||||
@@ -62,6 +66,7 @@ class InventoryDao:
|
||||
.where(BizPallet.pallet_id.in_(ordered_ids))
|
||||
.order_by(BizPallet.pallet_id)
|
||||
.with_for_update()
|
||||
.execution_options(populate_existing=True)
|
||||
)
|
||||
return sorted(result.scalars().all(), key=lambda pallet: pallet.pallet_id)
|
||||
|
||||
@@ -89,9 +94,13 @@ class InventoryDao:
|
||||
) -> BizPalletEvent:
|
||||
"""在调用方持有库存行锁时,为托盘追加下一序号事件。"""
|
||||
sequence_result = await db.execute(
|
||||
select(func.max(BizPalletEvent.event_seq)).where(BizPalletEvent.pallet_id == inventory.pallet_id)
|
||||
select(BizPalletEvent.event_seq)
|
||||
.where(BizPalletEvent.pallet_id == inventory.pallet_id)
|
||||
.order_by(BizPalletEvent.event_seq.desc())
|
||||
.limit(1)
|
||||
.with_for_update()
|
||||
)
|
||||
last_sequence = sequence_result.scalar_one()
|
||||
last_sequence = sequence_result.scalar_one_or_none()
|
||||
event.pallet_id = inventory.pallet_id
|
||||
event.event_seq = (last_sequence or 0) + 1
|
||||
db.add(event)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, time
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import ColumnElement, Select, delete, select, update
|
||||
@@ -45,10 +45,10 @@ class OperationDao:
|
||||
conditions.append(column >= begin_at)
|
||||
if end_time:
|
||||
end_at = datetime.combine(
|
||||
datetime.strptime(end_time, '%Y-%m-%d').date(),
|
||||
time(23, 59, 59),
|
||||
datetime.strptime(end_time, '%Y-%m-%d').date() + timedelta(days=1),
|
||||
time.min,
|
||||
)
|
||||
conditions.append(column <= end_at)
|
||||
conditions.append(column < end_at)
|
||||
return conditions
|
||||
|
||||
@staticmethod
|
||||
@@ -74,7 +74,12 @@ class OperationDao:
|
||||
record_id: int,
|
||||
*extra_conditions: ColumnElement[bool],
|
||||
) -> Any | None:
|
||||
result = await db.execute(select(model).where(id_column == record_id, *extra_conditions).with_for_update())
|
||||
result = await db.execute(
|
||||
select(model)
|
||||
.where(id_column == record_id, *extra_conditions)
|
||||
.with_for_update()
|
||||
.execution_options(populate_existing=True)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
@@ -135,7 +140,7 @@ class OperationDao:
|
||||
payload = {
|
||||
column.key: vars(record)[column.key]
|
||||
for column in model.__table__.columns
|
||||
if column.key != primary_key and column.key in vars(record)
|
||||
if column.key not in {primary_key, 'create_by', 'create_time'} and column.key in vars(record)
|
||||
}
|
||||
if payload:
|
||||
await db.execute(update(model).where(id_column == getattr(record, primary_key)).values(**payload))
|
||||
@@ -370,6 +375,7 @@ class OperationDao:
|
||||
|
||||
@classmethod
|
||||
async def delete_inbound(cls, db: AsyncSession, inbound_id: int) -> None:
|
||||
await db.execute(delete(BizInboundItem).where(BizInboundItem.inbound_id == inbound_id))
|
||||
await cls._delete(db, BizInboundOrder, BizInboundOrder.inbound_id, inbound_id)
|
||||
|
||||
@classmethod
|
||||
@@ -520,6 +526,7 @@ class OperationDao:
|
||||
|
||||
@classmethod
|
||||
async def delete_shipment(cls, db: AsyncSession, shipment_id: int) -> None:
|
||||
await db.execute(delete(BizShipmentItem).where(BizShipmentItem.shipment_id == shipment_id))
|
||||
await cls._delete(
|
||||
db,
|
||||
BizShipmentOrder,
|
||||
|
||||
@@ -16,6 +16,26 @@ def _ensure_unique_ids(value: list[int]) -> list[int]:
|
||||
return value
|
||||
|
||||
|
||||
def _parse_page_query_date(value: str) -> datetime:
|
||||
try:
|
||||
parsed = datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError as exc:
|
||||
raise ValueError('日期必须为有效的 YYYY-MM-DD 格式') from exc
|
||||
if parsed.strftime('%Y-%m-%d') != value:
|
||||
raise ValueError('日期必须为有效的 YYYY-MM-DD 格式')
|
||||
return parsed
|
||||
|
||||
|
||||
def _validate_page_query_date_range(value: BaseModel) -> BaseModel:
|
||||
begin_value = value.begin_time
|
||||
end_value = value.end_time
|
||||
begin_date = _parse_page_query_date(begin_value) if begin_value else None
|
||||
end_date = _parse_page_query_date(end_value) if end_value else None
|
||||
if begin_date and end_date and begin_date > end_date:
|
||||
raise ValueError('开始日期不能晚于结束日期')
|
||||
return value
|
||||
|
||||
|
||||
class InboundCreateModel(BaseModel):
|
||||
"""首次入库草稿新增模型。"""
|
||||
|
||||
@@ -335,6 +355,8 @@ class InboundPageQueryModel(BaseModel):
|
||||
begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间')
|
||||
end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间')
|
||||
|
||||
_validate_date_range = model_validator(mode='after')(_validate_page_query_date_range)
|
||||
|
||||
|
||||
class FillingPageQueryModel(BaseModel):
|
||||
"""托盘装填分页查询模型。"""
|
||||
@@ -351,6 +373,8 @@ class FillingPageQueryModel(BaseModel):
|
||||
begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间')
|
||||
end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间')
|
||||
|
||||
_validate_date_range = model_validator(mode='after')(_validate_page_query_date_range)
|
||||
|
||||
|
||||
class ShipmentPageQueryModel(BaseModel):
|
||||
"""整托发货分页查询模型。"""
|
||||
@@ -366,6 +390,8 @@ class ShipmentPageQueryModel(BaseModel):
|
||||
begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间')
|
||||
end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间')
|
||||
|
||||
_validate_date_range = model_validator(mode='after')(_validate_page_query_date_range)
|
||||
|
||||
|
||||
class UsagePageQueryModel(BaseModel):
|
||||
"""客户现场使用分页查询模型。"""
|
||||
@@ -381,6 +407,8 @@ class UsagePageQueryModel(BaseModel):
|
||||
begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间')
|
||||
end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间')
|
||||
|
||||
_validate_date_range = model_validator(mode='after')(_validate_page_query_date_range)
|
||||
|
||||
|
||||
class ReturnPageQueryModel(BaseModel):
|
||||
"""托盘退回分页查询模型。"""
|
||||
@@ -396,6 +424,8 @@ class ReturnPageQueryModel(BaseModel):
|
||||
begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间')
|
||||
end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间')
|
||||
|
||||
_validate_date_range = model_validator(mode='after')(_validate_page_query_date_range)
|
||||
|
||||
|
||||
class InboundScanQueryModel(BaseModel):
|
||||
"""首次入库扫码查询模型。"""
|
||||
|
||||
Reference in New Issue
Block a user