发布:将已验 TEST 功能更新至既有生产灰度(2026-10-23) #30
@@ -30,7 +30,7 @@
|
||||
|
||||
### 装车资料触发派送边界(2026-10-21)
|
||||
|
||||
`DispatchLoadingDocumentService`及平台/PDA两个装车资料Controller只引用`fleet.ownedbusiness.port.DispatchLoadingPhotoPort`的公开照片、版本、绑定请求和结果。`DispatchLoadingPhotoAdapter`在车队内部复用原附加表、上传账本和文件校验,不反向调用排车服务。排车服务持整车父锁,调用端口保存照片后在同一事务推进派送并记录原结果。结构门禁仅开放上述三类到此端口的精确引用,不开放车队Service、Repository或Mapper,也不新增包循环。
|
||||
`DispatchLoadingDocumentService`、`DispatchLoadingDepartureService`及平台/PDA两个装车资料Controller只引用`fleet.ownedbusiness.port.DispatchLoadingPhotoPort`的公开照片、版本、绑定请求和结果。`DispatchLoadingPhotoAdapter`在车队内部复用原附加表、上传账本和文件校验,不反向调用排车服务。排车服务持整车父锁,在装车确认或资料保存后核对两项条件,同一事务推进派送并记录原结果。结构门禁仅开放上述四类到此端口的精确引用,不开放车队Service、Repository或Mapper,也不新增包循环。
|
||||
|
||||
### 外配业务原排车详情读取边界(2026-10-21)
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ test('fleet stays a business slice and its policies have no infrastructure depen
|
||||
|| edge.source === 'com.baiyue.lastleg.dispatch.service.DispatchAggregateService'
|
||||
&& edge.target === 'com.baiyue.lastleg.fleet.ownedbusiness.port.OwnedBusinessOwnershipCapture'
|
||||
|| ['com.baiyue.lastleg.dispatch.service.DispatchLoadingDocumentService',
|
||||
'com.baiyue.lastleg.dispatch.service.DispatchLoadingDepartureService',
|
||||
'com.baiyue.lastleg.dispatch.controller.DispatchLoadingDocumentController',
|
||||
'com.baiyue.lastleg.dispatch.pda.controller.PdaDispatchLoadingDocumentController'].includes(edge.source)
|
||||
&& edge.target === 'com.baiyue.lastleg.fleet.ownedbusiness.port.DispatchLoadingPhotoPort'
|
||||
|
||||
+5
@@ -93,6 +93,11 @@ public interface DispatchSourceMapper {
|
||||
@Select("SELECT p.id FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id WHERE p.id=#{id} AND ip.customer_id=#{customer} FOR UPDATE")
|
||||
Long lockPlanPallet(@Param("customer") String customer, @Param("id") Long id);
|
||||
|
||||
/** 原计划完成实收后才能把预排估算替换为实际整托数量,避免收货中提前锁定半托。 */
|
||||
@Select("SELECT COUNT(*) FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id AND ip.customer_id=p.customer_id "
|
||||
+ "WHERE p.id=#{id} AND p.customer_id=#{customer} AND ip.status IN ('COMPLETED','PENDING_RECONCILIATION')")
|
||||
int receivedPlanCompleted(@Param("customer") String customer,@Param("id") Long id);
|
||||
|
||||
/** 退回来源仍使用本票实际实收,不借普通入仓别名。 */
|
||||
@Select("SELECT id FROM ll_return_stock_receipt WHERE id=#{id} AND customer_id=#{customer} FOR UPDATE")
|
||||
Long lockReturn(@Param("customer") String customer, @Param("id") Long id);
|
||||
|
||||
+2
@@ -74,6 +74,8 @@ public class DispatchSourceRepository {
|
||||
public Long lockOrder(String customer, Long id) { return source.lockOrder(customer,id); }
|
||||
/** 锁定真实计划托盘根。 */
|
||||
public Long lockPlan(String customer, Long id) { return source.lockPlanPallet(customer,id); }
|
||||
/** 只读核对原计划已经结束实际收货。 */
|
||||
public boolean receivedPlanCompleted(String customer,Long id){return source.receivedPlanCompleted(customer,id)==1;}
|
||||
/** 锁定实际退回收据根。 */
|
||||
public Long lockReturn(String customer, Long id) { return source.lockReturn(customer,id); }
|
||||
/** 真实库存数量及版本。 */
|
||||
|
||||
+2
-1
@@ -16,6 +16,7 @@ final class DispatchReceivedPlan {
|
||||
DispatchSourceRepository sources, DispatchService rules) {
|
||||
if(item.getWarehousePalletId()!=null || item.getPlanPalletId()==null
|
||||
|| !Objects.equals(warehouse,item.getOverseasWarehouseId()))return Optional.empty();
|
||||
if(!sources.receivedPlanCompleted(item.getCustomerId(),item.getPlanPalletId()))return Optional.empty();
|
||||
var rows=sources.selectedPlan(item.getCustomerId(),item.getPlanPalletId());
|
||||
if(rows.size()!=1)return Optional.empty();
|
||||
var actual=rows.get(0);
|
||||
@@ -31,7 +32,7 @@ final class DispatchReceivedPlan {
|
||||
if(selected==null || !Objects.equals(selected.getCustomerId(),member.customerId()))return Optional.empty();
|
||||
// 原计划的估算箱数不限制实收;已经绑定或换标的冻结贡献仍严格核对。
|
||||
if((selected.getWarehousePalletId()!=null || selected.getPlanPalletId()==null)
|
||||
&& !Objects.equals(Long.valueOf(selected.getPackageCount()),member.packages()))return Optional.empty();
|
||||
&& (selected.getPackageCount()==null || selected.getPackageCount().longValue()!=member.packages()))return Optional.empty();
|
||||
}
|
||||
var stock=sources.stock(item.getCustomerId(),actual.warehousePalletId(),false);
|
||||
if(stock.stream().anyMatch(p->!Objects.equals(p.warehouseId(),warehouse)
|
||||
|
||||
+11
@@ -23,6 +23,16 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
/** 根版本、幂等、真实客户作业与扫码持久化;库存/归属应用命令在此明确模拟,另由原域直接测试验证。 */
|
||||
class DispatchPickingPersistenceTest {
|
||||
/** 数量可自然同步不代表能接管其他车的实际托盘。 */
|
||||
@Test void receivedPalletClaimedByAnotherDispatchCannotBePicked() throws Exception {
|
||||
var fixture=new Picking(true);fixture.receivedPlans(9,11);
|
||||
fixture.f.db.update("UPDATE ll_dispatch_physical_pallet_lock SET dispatch_order_id=999 WHERE warehouse_pallet_id=501");
|
||||
assertThatThrownBy(()->fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.item,
|
||||
new DispatchCommands.Pick(fixture.version,"occupied-pallet",11L,9,0,null,null))))
|
||||
.isInstanceOf(com.baiyue.common.core.exception.BizException.class).hasMessageContaining("占用");
|
||||
verify(fixture.inventory,never()).ship(any(),any(),any());
|
||||
assertThat(fixture.f.db.queryForList("SELECT package_count FROM ll_dispatch_item ORDER BY customer_id",Integer.class)).containsExactly(10,10);
|
||||
}
|
||||
/** 预排两托10+10、实收9+11,首次扫码同步真实数量并保留原审核,原键重放不重复扣货。 */
|
||||
@Test void receivedPalletCountsReplaceEstimatesAtomically() throws Exception {
|
||||
var fixture=new Picking(true);fixture.receivedPlans(9,11);
|
||||
@@ -139,6 +149,7 @@ class DispatchPickingPersistenceTest {
|
||||
f.candidates.get(plan).packageCount(count);
|
||||
f.db.update("UPDATE ll_inventory_position SET available_packages=? WHERE id=?",count,10+n);
|
||||
doReturn(List.of(f.candidates.get(plan))).when(sources).selectedPlan(customer,plan);
|
||||
doReturn(true).when(sources).receivedPlanCompleted(customer,plan);
|
||||
doReturn(List.of(9000+plan)).when(sources).sourceOrders(customer,null,plan);
|
||||
doReturn(List.of(9000+plan)).when(sources).sourceOrders(customer,pallet,plan);
|
||||
doReturn(plan).when(sources).lockPlan(customer,plan);
|
||||
|
||||
+25
-1
@@ -17,6 +17,22 @@ import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
/** 真实PDA任务投影衔接原计划;来源提供者隔离,GET不得写绑定或库存。 */
|
||||
class DispatchPlanPdaReadPersistenceTest {
|
||||
/** 完成状态查询走真实Mapper,未完成或其他客户的计划不能被当作本托实收。 */
|
||||
@Test void completionGateUsesOriginalCustomerAndPlan() throws Exception {
|
||||
var f=new Fixture();
|
||||
f.f.db.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR(64),status VARCHAR(64))");
|
||||
f.f.db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT,customer_id VARCHAR(64))");
|
||||
f.f.db.update("INSERT INTO ll_inbound_plan VALUES(1,'101','IN_PROGRESS')");
|
||||
f.f.db.update("INSERT INTO ll_inbound_plan_pallet VALUES(2,1,'101')");
|
||||
var mapper=f.f.session.getMapper(com.baiyue.lastleg.dispatch.mapper.DispatchSourceMapper.class);
|
||||
assertThat(mapper.receivedPlanCompleted("101",2L)).isZero();
|
||||
for(var status:List.of("COMPLETED","PENDING_RECONCILIATION")) {
|
||||
f.f.db.update("UPDATE ll_inbound_plan SET status=?",status);
|
||||
assertThat(mapper.receivedPlanCompleted("101",2L)).isOne();
|
||||
assertThat(mapper.receivedPlanCompleted("other",2L)).isZero();
|
||||
assertThat(mapper.receivedPlanCompleted("101",3L)).isZero();
|
||||
}
|
||||
}
|
||||
/** 已实收原计划直接提供首拣仓位,查询不推进原车版本或写入实托。 */
|
||||
@Test void receivedPlanExposesPositionWithoutMaterializing() throws Exception {
|
||||
var f=new Fixture();var before=f.dbState();
|
||||
@@ -61,11 +77,18 @@ class DispatchPlanPdaReadPersistenceTest {
|
||||
@Test void actualStockMustMatchWarehousePalletAndQuantity() throws Exception {
|
||||
var f=new Fixture();
|
||||
for(var stock:List.of(new Stock(11,21,501L,"99",3,0,0,0,1),
|
||||
new Stock(11,21,502L,"9",3,0,0,0,1),new Stock(11,21,501L,"9",2,0,0,0,1))) {
|
||||
new Stock(11,21,502L,"9",3,0,0,0,1),new Stock(11,21,501L,"9",2,0,0,0,1),
|
||||
new Stock(11,21,501L,"9",3,1,0,0,1),new Stock(11,21,501L,"9",3,0,1,0,1))) {
|
||||
when(f.sources.stock("101",501L,false)).thenReturn(List.of(stock));
|
||||
assertThat(f.read().items().get(0).positions()).isEmpty();
|
||||
}
|
||||
}
|
||||
/** 收货尚未结束时不把当前部分实收数量当成整托最终数量。 */
|
||||
@Test void receivingPlanDoesNotExposePartialPallet() throws Exception {
|
||||
var f=new Fixture();when(f.sources.receivedPlanCompleted("101",1L)).thenReturn(false);
|
||||
assertThat(f.read().items().get(0).positions()).isEmpty();
|
||||
verify(f.sources,never()).stock(anyString(),anyLong(),anyBoolean());
|
||||
}
|
||||
/** 复用真实排车及PDA Mapper,模拟原计划来源快照。 */
|
||||
private static final class Fixture {
|
||||
final DispatchAggregatePersistenceTest.Fixture f=new DispatchAggregatePersistenceTest.Fixture();
|
||||
@@ -88,6 +111,7 @@ class DispatchPlanPdaReadPersistenceTest {
|
||||
queries=new DispatchPdaQueryService(new DispatchPdaReadRepository(new SqlSessionTemplate(factory.getObject()).getMapper(DispatchWholePdaReadMapper.class)),f.service);
|
||||
ReflectionTestUtils.setField(queries,"sources",sources);ReflectionTestUtils.setField(queries,"sourceRules",rules);
|
||||
when(sources.selectedPlan("101",1L)).thenReturn(List.of(f.candidates.get(1L)));
|
||||
when(sources.receivedPlanCompleted(anyString(),anyLong())).thenReturn(true);
|
||||
when(sources.sourceOrders("101",null,1L)).thenReturn(List.of(9001L));
|
||||
when(sources.sourceOrders("101",501L,1L)).thenReturn(List.of(9001L));
|
||||
when(sources.stock("101",501L,false)).thenReturn(List.of(new Stock(11,21,501L,"9",3,0,0,0,1)));
|
||||
|
||||
Reference in New Issue
Block a user