From 317a0fbf68357ddc9405f5f0b5a22b0ed90a95f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A6=E9=BE=99=E8=A8=80?= Date: Sun, 26 Jul 2026 14:24:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E9=A6=96=E4=BA=8B=E4=BB=B6=E9=97=B4=E9=9A=99=E9=94=81=E6=AD=BB?= =?UTF-8?q?=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_business/dao/inventory_dao.py | 23 ++++++++------ .../module_business/entity/vo/operation_vo.py | 2 ++ .../module_business/test_inventory_dao.py | 31 ++++++++++++++++--- .../tests/module_business/test_vo_contract.py | 12 +++++++ 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/shierfeng-fastapi-backend/module_business/dao/inventory_dao.py b/shierfeng-fastapi-backend/module_business/dao/inventory_dao.py index 0b273d7..c960ede 100644 --- a/shierfeng-fastapi-backend/module_business/dao/inventory_dao.py +++ b/shierfeng-fastapi-backend/module_business/dao/inventory_dao.py @@ -93,16 +93,21 @@ class InventoryDao: event: BizPalletEvent, ) -> BizPalletEvent: """在调用方持有库存行锁时,为托盘追加下一序号事件。""" - sequence_result = await db.execute( - 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_or_none() + last_sequence = 0 + if inventory.last_event_id is not None: + sequence_result = await db.execute( + select(BizPalletEvent.event_seq) + .where( + BizPalletEvent.event_id == inventory.last_event_id, + BizPalletEvent.pallet_id == inventory.pallet_id, + ) + .with_for_update() + ) + last_sequence = sequence_result.scalar_one_or_none() + if last_sequence is None: + raise RuntimeError('托盘库存事件指针不一致') event.pallet_id = inventory.pallet_id - event.event_seq = (last_sequence or 0) + 1 + event.event_seq = last_sequence + 1 db.add(event) await db.flush() inventory.last_event_id = event.event_id diff --git a/shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py b/shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py index 4142c49..839a3fc 100644 --- a/shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py +++ b/shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py @@ -31,6 +31,8 @@ def _validate_page_query_date_range(value: BaseModel) -> BaseModel: 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 end_date and end_date.date() == datetime.max.date(): + raise ValueError('结束日期必须早于 9999-12-31') if begin_date and end_date and begin_date > end_date: raise ValueError('开始日期不能晚于结束日期') return value diff --git a/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py b/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py index 92a207d..e4f78c6 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py @@ -284,12 +284,12 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase): self.assertIn("biz_pallet_event.idempotency_key = 'event-key'", sql) self.assertNotIn('FOR UPDATE', sql) - async def test_add_event_allocates_next_sequence_and_updates_inventory_pointer(self) -> None: + async def test_add_event_reads_exact_previous_pointer_and_updates_inventory(self) -> None: sequence_result = MagicMock() sequence_result.scalar_one.return_value = 4 sequence_result.scalar_one_or_none.return_value = 4 self.db.execute.return_value = sequence_result - inventory = BizPalletInventory(pallet_id=7, last_event_id=None) + inventory = BizPalletInventory(pallet_id=7, last_event_id=40) event = BizPalletEvent(pallet_id=99, event_seq=99, idempotency_key='event-key') flush_count = 0 @@ -311,15 +311,17 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase): self.assertEqual(2, self.db.flush.await_count) statement = self.db.execute.await_args.args[0] self.assertTrue(statement.selected_columns[0].compare(BizPalletEvent.event_seq.property.columns[0])) + self.assertTrue(statement.get_final_froms()[0].compare(BizPalletEvent.__table__)) for dialect in (mysql.dialect(), postgresql.dialect()): sql = compile_sql(statement, dialect) self.assertNotIn('max(', sql.lower()) + self.assertIn('biz_pallet_event.event_id = 40', sql) self.assertIn('biz_pallet_event.pallet_id = 7', sql) - self.assertIn('ORDER BY biz_pallet_event.event_seq DESC', sql) - self.assertIn('LIMIT 1', sql) self.assertIn('FOR UPDATE', sql) + self.assertNotIn('ORDER BY', sql) + self.assertNotIn('LIMIT', sql) - async def test_add_first_event_starts_sequence_at_one(self) -> None: + async def test_add_first_event_starts_at_one_without_query(self) -> None: sequence_result = MagicMock() sequence_result.scalar_one.return_value = None sequence_result.scalar_one_or_none.return_value = None @@ -331,6 +333,25 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase): self.assertEqual(1, event.event_seq) self.assertEqual(51, inventory.last_event_id) + self.db.execute.assert_not_awaited() + self.db.add.assert_called_once_with(event) + self.assertEqual(2, self.db.flush.await_count) + + async def test_add_event_rejects_missing_previous_pointer_without_write(self) -> None: + sequence_result = MagicMock() + sequence_result.scalar_one_or_none.return_value = None + self.db.execute.return_value = sequence_result + inventory = BizPalletInventory(pallet_id=8, last_event_id=50) + event = BizPalletEvent(pallet_id=8, idempotency_key='broken-pointer') + + with self.assertRaisesRegex(RuntimeError, '托盘库存事件指针不一致'): + await InventoryDao.add_event(self.db, inventory, event) + + self.db.execute.assert_awaited_once() + self.db.add.assert_not_called() + self.db.flush.assert_not_awaited() + self.assertEqual(50, inventory.last_event_id) + self.assertIsNone(event.event_seq) class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase): diff --git a/shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py b/shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py index eed506b..ba07c54 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py @@ -809,6 +809,18 @@ class BusinessVoContractTest(unittest.TestCase): with self.subTest(model=model_class.__name__, case='inverted'), self.assertRaises(ValidationError): model_class.model_validate({'beginTime': '2026-07-03', 'endTime': '2026-07-02'}) + def test_operation_page_query_rejects_overflowing_end_but_allows_same_begin(self) -> None: + for model_class in OPERATION_PAGE_QUERY_MODELS: + with self.subTest(model=model_class.__name__, field='beginTime'): + model = model_class.model_validate({'beginTime': '9999-12-31'}) + self.assertEqual('9999-12-31', model.begin_time) + self.assertIsNone(model.end_time) + with ( + self.subTest(model=model_class.__name__, field='endTime'), + self.assertRaises(ValidationError), + ): + model_class.model_validate({'endTime': '9999-12-31'}) + def test_pallet_page_size_is_capped_at_100(self) -> None: self.assertEqual(100, PalletPageQueryModel.model_validate({'pageSize': 100}).page_size) with self.assertRaises(ValidationError):