From f3d57869f2f80b52912ff355218c000be57becb4 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 28 Oct 2026 13:57:34 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=B9=E7=94=A8=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lastleg-service/docs/procurement-workflow.md | 4 +-- .../service/ProcurementWorkflowService.java | 12 ------- .../openapi/procurement-workflow-v1.yaml | 2 +- .../ProcurementWorkflowPersistenceTest.java | 31 ++----------------- 4 files changed, 6 insertions(+), 43 deletions(-) diff --git a/lastleg-service/docs/procurement-workflow.md b/lastleg-service/docs/procurement-workflow.md index f9f257721..b94e378cb 100644 --- a/lastleg-service/docs/procurement-workflow.md +++ b/lastleg-service/docs/procurement-workflow.md @@ -20,7 +20,7 @@ 主单版本、客户隔离、附件归属与幂等键由服务端复核。失败不能留下部分批次或库存变动;同幂等键不同载荷必须拒绝,过期版本须刷新后重新操作。 -费用明细使用其他海外业务共用的费用窗口和费用来源,采购默认选择余额支付。每次创建出库批次前,本采购主单必须至少有一项未作废应收费用,且全部未作废应收费用均须有效并审核通过;缺少费用、待审核或驳回时提示并阻止出库,前后端共同校验。应付与已作废费用不能替代应收费用明细;原出库成功请求的幂等重放不重新创建批次。余额冻结、审核实扣、失败重试和扣款回执沿用海外人工费用链;审核通过与扣款成功分别展示,创建费用或发出资金指令不能显示为已扣款。采购履约不以旧的预算核验、装箱、面单或两段运费冻结作为门禁。 +费用明细使用其他海外业务共用的费用窗口和费用来源,采购默认选择余额支付。费用明细缺失或尚未审核时可提示运营补充,但不阻止创建出库批次;出库资格只由采购状态、可用数量、物流跟踪号及操作权限决定。原出库成功请求的幂等重放不重新创建批次。余额冻结、审核实扣、失败重试和扣款回执沿用海外人工费用链;审核通过与扣款成功分别展示,创建费用或发出资金指令不能显示为已扣款。采购履约不以旧的预算核验、装箱、面单或两段运费冻结作为门禁。 客户端保留独立下单、列表和详情路由。详情集中展示商品及收件信息、收货批次、出库批次和费用,待客户确认状态提供确认动作。平台列表突出主状态及数量,详情承载审核、收货、提交确认、出库和费用操作;不重复展示旧节点表单。 @@ -34,7 +34,7 @@ - 分两批出库;超量、并发、过期版本和异载荷重放被拒绝。 - 批次派送后上传文件或图片签收,库存仅扣一次;未完成批次与剩余库存阻止整单提前完成。 - 费用默认余额支付,费用归属本单客户,余额结果以资金回执为准。 -- 无费用、仅作废费用、仅应付费用或任一应收尚未审核通过时,不能创建出库;全部有效应收审核通过后允许出库。 +- 无费用、费用尚未审核或驳回时均允许创建出库;页面提示不影响出库操作。 自动测试、本地类型检查、真实 HTTP 与页面验收分别记录。现有 `procurement-sample-fulfillment.mjs` 验证旧两段样品链,不能作为本流程验收通过的依据。 diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementWorkflowService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementWorkflowService.java index 7e03c0ce6..bf4db1291 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementWorkflowService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementWorkflowService.java @@ -5,7 +5,6 @@ import com.baiyue.lastleg.procurement.dto.ProcurementWorkflowDtos.*; import com.baiyue.lastleg.procurement.entity.OverseasProcurementOrder; import com.baiyue.lastleg.procurement.repository.ProcurementWorkflowRepository; import com.baiyue.lastleg.service.LastlegDocumentNumberGenerator; -import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,7 +22,6 @@ public class ProcurementWorkflowService { private final ProcurementWorkflowRepository repository; private final LastlegDocumentNumberGenerator numbers; private final ObjectMapper json; - private final OverseasFeeSourceService fees; /** 客户和平台共享实际批次事实,按客户及单号读取。 */ @Transactional(readOnly=true) @@ -73,7 +71,6 @@ public class ProcurementWorkflowService { var order=order(customer,no,true); if(replay(order,request.idempotencyKey(),"OUTBOUND",request)) return snapshot(order); version(order,request.version());state(order,"CONFIRMED","IN_TRANSIT","DELIVERING"); - requireReviewedFees(order); if(request.quantity()<=0 || request.quantity()>snapshot(order).availableQuantity()) throw conflict("出库数量超过采购可用库存"); if(request.trackingNo()==null || request.trackingNo().isBlank() || request.trackingNo().length()>255) throw bad("请填写物流跟踪号"); repository.outbound(IdWorker.getId(),order.getId(),numbers.next("CK"),request.quantity(),request.trackingNo().trim(),operator); @@ -82,15 +79,6 @@ public class ProcurementWorkflowService { return snapshot(order); } - /** 本单有效应收须先添加并全部审核通过,作废费用不能替代费用明细。 */ - private void requireReviewedFees(OverseasProcurementOrder order) { - var current=fees.listForBusiness(order.getCustomerId(),"OVERSEAS_PROCUREMENT",order.getId()).stream() - .filter(f->"RECEIVABLE".equals(f.getDirection()) && !"VOIDED".equals(f.getSourceStatus())).toList(); - if(current.isEmpty()) throw conflict("请先添加费用明细并审核通过后再出库"); - if(current.stream().anyMatch(f->!"ACTIVE".equals(f.getSourceStatus()) || !"APPROVED".equals(f.getReviewStatus()))) - throw conflict("费用明细尚未全部审核通过,请先完成费用审核后再出库"); - } - /** 按具体出库批次推进到派送中。 */ @Transactional public Workflow dispatch(String customer,String no,long batchId,String operator,Command request) { diff --git a/lastleg-service/src/main/resources/openapi/procurement-workflow-v1.yaml b/lastleg-service/src/main/resources/openapi/procurement-workflow-v1.yaml index a5f94417b..653e54b37 100644 --- a/lastleg-service/src/main/resources/openapi/procurement-workflow-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/procurement-workflow-v1.yaml @@ -63,7 +63,7 @@ paths: /api/platform/v1/last-leg/overseas-procurements/{orderNo}/outbounds: post: summary: 从采购库存创建出库单 - description: 权限 platform:overseas-business:purchase:ship-customer;仅平台主体,必须指定目标客户 customerId。仅 CONFIRMED、IN_TRANSIT 或 DELIVERING;数量不得超过 availableQuantity,物流跟踪号必填。必须存在本采购单未作废应收费用且全部有效并审核通过;缺少费用、待审核或驳回时拒绝出库。创建后占用数量增加,不实际扣库存。 + description: 权限 platform:overseas-business:purchase:ship-customer;仅平台主体,必须指定目标客户 customerId。仅 CONFIRMED、IN_TRANSIT 或 DELIVERING;数量不得超过 availableQuantity,物流跟踪号必填。费用明细缺失或尚未审核仅作提示,不阻止创建出库单。创建后占用数量增加,不实际扣库存。 parameters: - $ref: '#/components/parameters/OrderNo' - $ref: '#/components/parameters/CustomerId' diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementWorkflowPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementWorkflowPersistenceTest.java index c4c06bc30..54ce44d4a 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementWorkflowPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementWorkflowPersistenceTest.java @@ -7,8 +7,6 @@ import com.baiyue.lastleg.procurement.entity.*; import com.baiyue.lastleg.procurement.mapper.*; import com.baiyue.lastleg.procurement.repository.ProcurementWorkflowRepository; import com.baiyue.lastleg.service.LastlegDocumentNumberGenerator; -import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService; -import com.baiyue.lastleg.financefee.entity.OverseasFeeSource; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.*; import org.mybatis.spring.SqlSessionTemplate; @@ -33,7 +31,6 @@ class ProcurementWorkflowPersistenceTest { OverseasProcurementFileMapper files; OverseasProcurementStatusHistoryMapper histories; ProcurementWorkflowRepository repository; - OverseasFeeSourceService fees; @BeforeEach void setup() throws Exception { var ds=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1","sa",""); @@ -49,9 +46,7 @@ class ProcurementWorkflowPersistenceTest { histories=session.getMapper(OverseasProcurementStatusHistoryMapper.class); repository=new ProcurementWorkflowRepository(session.getMapper(ProcurementWorkflowMapper.class),orders,files); var numbers=mock(LastlegDocumentNumberGenerator.class);var sequence=new AtomicInteger();when(numbers.next("CK")).thenAnswer(i->"CK-"+sequence.incrementAndGet()); - fees=mock(OverseasFeeSourceService.class); - when(fees.listForBusiness("9","OVERSEAS_PROCUREMENT",11L)).thenReturn(List.of(fee("ACTIVE","APPROVED"))); - service=new ProcurementWorkflowService(repository,numbers,new ObjectMapper().findAndRegisterModules(),fees); + service=new ProcurementWorkflowService(repository,numbers,new ObjectMapper().findAndRegisterModules()); var order=new OverseasProcurementOrder();order.setId(11L);order.setCustomerId("9");order.setOrderNo("CG-1");order.setProductName("采购商品");order.setPurchaseCategory("");order.setPurchaseQuantity(10L);order.setCurrency("USD");order.setStatus("RECEIVING");order.setVersion(0L);order.setInboundQuantity(0L);order.setContactName("buyer");order.setContactPhone("123456789");order.setIdempotencyKey("create");order.setRequestHash("0".repeat(64));orders.insert(order); file(101L,11L,"9","image/jpeg");file(102L,11L,"9","application/pdf");file(103L,12L,"10","image/jpeg");file(104L,11L,"9","image/png"); } @@ -131,34 +126,14 @@ class ProcurementWorkflowPersistenceTest { assertThat(service.read("9","CG-1").stockQuantity()).isEqualTo(10); } - @Test void outboundRequiresCurrentReviewedReceivablesAndRejectedAttemptsLeaveNoInventoryChanges() { + @Test void outboundDoesNotRequireFeeDetailsOrApproval() { tx.execute(s->service.receive("9","CG-1","7",new Receipt(0,"r",10,List.of(101L)))); tx.execute(s->service.requestConfirmation("9","CG-1","7",new Command(1,"ask"))); tx.execute(s->service.confirm("9","CG-1","8",new Command(2,"ok"))); var request=new Outbound(3,"out",3,"TRACK"); - for(var rows:List.of(List.of(),List.of(fee("VOIDED","APPROVED")))) { - when(fees.listForBusiness("9","OVERSEAS_PROCUREMENT",11L)).thenReturn(rows); - assertThatThrownBy(()->tx.execute(s->service.outbound("9","CG-1","7",request))).hasMessageContaining("请先添加费用明细"); - } - var payable=fee("ACTIVE","APPROVED");payable.setDirection("PAYABLE"); - when(fees.listForBusiness("9","OVERSEAS_PROCUREMENT",11L)).thenReturn(List.of(payable)); - assertThatThrownBy(()->tx.execute(s->service.outbound("9","CG-1","7",request))).hasMessageContaining("请先添加费用明细"); - for(String review:List.of("PENDING","REJECTED")) { - when(fees.listForBusiness("9","OVERSEAS_PROCUREMENT",11L)).thenReturn(List.of(fee("ACTIVE","APPROVED"),fee("ACTIVE",review))); - assertThatThrownBy(()->tx.execute(s->service.outbound("9","CG-1","7",request))).hasMessageContaining("尚未全部审核通过"); - } - assertThat(service.read("9","CG-1").version()).isEqualTo(3); - assertThat(service.read("9","CG-1").reservedQuantity()).isZero(); - assertThat(service.read("9","CG-1").outbounds()).isEmpty(); - when(fees.listForBusiness("9","OVERSEAS_PROCUREMENT",11L)).thenReturn(List.of(fee("ACTIVE","APPROVED"),fee("VOIDED","REJECTED"))); var result=tx.execute(s->service.outbound("9","CG-1","7",request)); assertThat(result.reservedQuantity()).isEqualTo(3); - when(fees.listForBusiness("9","OVERSEAS_PROCUREMENT",11L)).thenReturn(List.of()); - assertThat(tx.execute(s->service.outbound("9","CG-1","7",request)).version()).isEqualTo(4); - } - - private OverseasFeeSource fee(String status,String review) { - var fee=new OverseasFeeSource();fee.setDirection("RECEIVABLE");fee.setSourceStatus(status);fee.setReviewStatus(review);return fee; + assertThat(result.outbounds()).hasSize(1); } private void file(long id,long orderId,String customer,String type) {