Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test
This commit is contained in:
@@ -34,6 +34,17 @@ public class FulfillmentTaskRepository {
|
||||
.eq(FulfillmentTaskEntity::getOrderId, orderId));
|
||||
}
|
||||
|
||||
public FulfillmentTaskEntity findByOrderForUpdate(long tenantId, long orderId) {
|
||||
return tasks.selectOne(new LambdaQueryWrapper<FulfillmentTaskEntity>()
|
||||
.eq(FulfillmentTaskEntity::getTenantId, tenantId)
|
||||
.eq(FulfillmentTaskEntity::getOrderId, orderId)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public void insert(FulfillmentTaskEntity task) {
|
||||
tasks.insert(task);
|
||||
}
|
||||
|
||||
public PageResult<FulfillmentTaskEntity> page(long tenantId, FulfillmentTaskQuery query) {
|
||||
LambdaQueryWrapper<FulfillmentTaskEntity> wrapper = new LambdaQueryWrapper<FulfillmentTaskEntity>()
|
||||
.eq(FulfillmentTaskEntity::getTenantId, tenantId)
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
|
||||
import com.baiyue.firstleg.repository.FulfillmentTaskRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
/** 将订单审核快照幂等转换为待分配履约任务。 */
|
||||
@Service
|
||||
public class FulfillmentTaskProvisioningService {
|
||||
private final FulfillmentTaskRepository repository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public FulfillmentTaskProvisioningService(FulfillmentTaskRepository repository, ObjectMapper objectMapper) {
|
||||
this.repository = repository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public FulfillmentTaskEntity provision(OrderApprovedInboundCommand command, LocalDateTime now) {
|
||||
FulfillmentTaskEntity existing = repository.findByOrder(command.tenantId(), command.orderId());
|
||||
if (existing != null) {
|
||||
return requireMatchingOrder(existing, command.orderNo());
|
||||
}
|
||||
|
||||
FulfillmentTaskEntity task = new FulfillmentTaskEntity();
|
||||
task.setId(IdWorker.getId());
|
||||
task.setTenantId(command.tenantId());
|
||||
task.setOrderId(command.orderId());
|
||||
task.setOrderNo(command.orderNo());
|
||||
task.setOrderSnapshotJson(snapshot(command));
|
||||
task.setStatus("PENDING_ASSIGNMENT");
|
||||
task.setCreatedBy(0L);
|
||||
task.setUpdatedBy(0L);
|
||||
task.setCreatedAt(now);
|
||||
task.setUpdatedAt(now);
|
||||
task.setDeleted(false);
|
||||
task.setVersion(0);
|
||||
try {
|
||||
repository.insert(task);
|
||||
return task;
|
||||
} catch (DuplicateKeyException duplicate) {
|
||||
FulfillmentTaskEntity winner = repository.findByOrderForUpdate(
|
||||
command.tenantId(), command.orderId());
|
||||
if (winner == null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "履约任务已被并发创建但无法读取");
|
||||
}
|
||||
return requireMatchingOrder(winner, command.orderNo());
|
||||
}
|
||||
}
|
||||
|
||||
private String snapshot(OrderApprovedInboundCommand command) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(command);
|
||||
} catch (JsonProcessingException exception) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "订单履约快照序列化失败");
|
||||
}
|
||||
}
|
||||
|
||||
private FulfillmentTaskEntity requireMatchingOrder(FulfillmentTaskEntity task, String orderNo) {
|
||||
if (!Objects.equals(task.getOrderNo(), orderNo)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "订单号与已有履约任务不一致");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
}
|
||||
+32
-1
@@ -7,6 +7,7 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.firstleg.dto.InboundOrderReference;
|
||||
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
@@ -25,18 +26,25 @@ public class InboundOrderCreationService {
|
||||
private InboundOrderMapper orders;
|
||||
@Resource
|
||||
private InboundStatusHistoryMapper histories;
|
||||
@Resource
|
||||
private FulfillmentTaskProvisioningService fulfillmentTasks;
|
||||
|
||||
@Transactional
|
||||
public long createFromApprovedOrder(OrderApprovedInboundCommand command) {
|
||||
InboundOrderEntity existing = findByOrder(command.tenantId(), command.orderId(), false);
|
||||
if (existing != null) {
|
||||
return requireMatchingInboundCode(existing, command.inboundCode());
|
||||
long inboundId = requireMatchingInboundCode(existing, command.inboundCode());
|
||||
FulfillmentTaskEntity task = fulfillmentTasks.provision(command, LocalDateTime.now());
|
||||
linkTask(existing, task.getId());
|
||||
return inboundId;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
FulfillmentTaskEntity task = fulfillmentTasks.provision(command, now);
|
||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||
inbound.setId(IdWorker.getId());
|
||||
inbound.setTenantId(command.tenantId());
|
||||
inbound.setTaskId(task.getId());
|
||||
inbound.setOrderId(command.orderId());
|
||||
inbound.setOrderNo(command.orderNo());
|
||||
inbound.setInboundCode(command.inboundCode());
|
||||
@@ -116,6 +124,29 @@ public class InboundOrderCreationService {
|
||||
return existing.getId();
|
||||
}
|
||||
|
||||
private void linkTask(InboundOrderEntity inbound, long taskId) {
|
||||
if (inbound.getTaskId() != null) {
|
||||
if (!inbound.getTaskId().equals(taskId)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入库单已关联其他履约任务");
|
||||
}
|
||||
return;
|
||||
}
|
||||
int updated = orders.update(null,
|
||||
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<InboundOrderEntity>()
|
||||
.eq(InboundOrderEntity::getTenantId, inbound.getTenantId())
|
||||
.eq(InboundOrderEntity::getId, inbound.getId())
|
||||
.isNull(InboundOrderEntity::getTaskId)
|
||||
.set(InboundOrderEntity::getTaskId, taskId));
|
||||
if (updated == 1) {
|
||||
inbound.setTaskId(taskId);
|
||||
return;
|
||||
}
|
||||
InboundOrderEntity current = findByOrder(inbound.getTenantId(), inbound.getOrderId(), true);
|
||||
if (current == null || !Long.valueOf(taskId).equals(current.getTaskId())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入库单履约任务关联已发生变化");
|
||||
}
|
||||
}
|
||||
|
||||
public List<InboundOrderReference.Item> findReferences(long tenantId, List<Long> orderIds) {
|
||||
if (orderIds.isEmpty()) return List.of();
|
||||
return orders.selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<InboundOrderEntity>()
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
|
||||
import com.baiyue.firstleg.repository.FulfillmentTaskRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FulfillmentTaskProvisioningServiceTest {
|
||||
private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 8, 10, 0);
|
||||
@Mock private FulfillmentTaskRepository repository;
|
||||
private FulfillmentTaskProvisioningService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new FulfillmentTaskProvisioningService(repository, new ObjectMapper().findAndRegisterModules());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreatePendingTaskFromApprovedOrderSnapshot() {
|
||||
FulfillmentTaskEntity created = service.provision(command(), NOW);
|
||||
|
||||
ArgumentCaptor<FulfillmentTaskEntity> captor = ArgumentCaptor.forClass(FulfillmentTaskEntity.class);
|
||||
verify(repository).insert(captor.capture());
|
||||
assertThat(created).isSameAs(captor.getValue());
|
||||
assertThat(created.getTenantId()).isEqualTo(10001L);
|
||||
assertThat(created.getOrderId()).isEqualTo(77L);
|
||||
assertThat(created.getOrderNo()).isEqualTo("FL-77");
|
||||
assertThat(created.getStatus()).isEqualTo("PENDING_ASSIGNMENT");
|
||||
assertThat(created.getOrderSnapshotJson()).contains("\"tenantId\":10001", "\"orderId\":77", "\"inboundCode\":\"RK-77\"");
|
||||
assertThat(created.getCreatedAt()).isEqualTo(NOW);
|
||||
assertThat(created.getDeleted()).isFalse();
|
||||
assertThat(created.getVersion()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnExistingTaskForIdempotentReplay() {
|
||||
FulfillmentTaskEntity existing = task(700L);
|
||||
when(repository.findByOrder(10001L, 77L)).thenReturn(existing);
|
||||
|
||||
assertThat(service.provision(command(), NOW)).isSameAs(existing);
|
||||
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConvergeOnConcurrentTaskCreation() {
|
||||
FulfillmentTaskEntity winner = task(701L);
|
||||
doThrow(new DuplicateKeyException("concurrent insert")).when(repository).insert(any());
|
||||
when(repository.findByOrderForUpdate(10001L, 77L)).thenReturn(winner);
|
||||
|
||||
assertThat(service.provision(command(), NOW)).isSameAs(winner);
|
||||
}
|
||||
|
||||
private static FulfillmentTaskEntity task(long id) {
|
||||
FulfillmentTaskEntity task = new FulfillmentTaskEntity();
|
||||
task.setId(id); task.setTenantId(10001L); task.setOrderId(77L); task.setOrderNo("FL-77");
|
||||
return task;
|
||||
}
|
||||
|
||||
private static OrderApprovedInboundCommand command() {
|
||||
return new OrderApprovedInboundCommand(
|
||||
10001L, 77L, "FL-77", "RK-77", 7L, "测试客户", 9L, "义乌仓",
|
||||
null, null, null, "FBA", "AIR", "空运", null, null,
|
||||
"BUY_DECLARATION", "TAX_EXCLUDED", null, null, "FBA", null,
|
||||
"ONT8", "ONT8", "验收备注", null, null);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,18 @@ class FulfillmentTaskServiceTest {
|
||||
.isEqualTo(ResultCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("按客户和订单查询返回审核后生成的履约任务")
|
||||
void shouldReturnProvisionedTaskByTenantAndOrder() {
|
||||
FulfillmentTaskEntity task = new FulfillmentTaskEntity();
|
||||
task.setId(700L); task.setTenantId(10001L); task.setOrderId(77L);
|
||||
when(repository.findByOrder(10001L, 77L)).thenReturn(task);
|
||||
|
||||
assertThat(service.byOrder(10001L, 77L)).isSameAs(task);
|
||||
|
||||
verify(repository).findByOrder(10001L, 77L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询分配历史前先校验任务归属租户")
|
||||
void shouldVerifyTaskTenantBeforeLoadingAssignmentHistory() {
|
||||
|
||||
+41
@@ -1,15 +1,20 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
@@ -26,10 +31,18 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundOrderCreationServiceTest {
|
||||
@BeforeAll
|
||||
static void initializeMetadata() {
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), InboundOrderEntity.class);
|
||||
}
|
||||
|
||||
@Mock
|
||||
private InboundOrderMapper orders;
|
||||
@Mock
|
||||
private InboundStatusHistoryMapper histories;
|
||||
@Mock
|
||||
private FulfillmentTaskProvisioningService fulfillmentTasks;
|
||||
@InjectMocks
|
||||
private InboundOrderCreationService service;
|
||||
|
||||
@@ -41,7 +54,9 @@ class InboundOrderCreationServiceTest {
|
||||
existing.setTenantId(10001L);
|
||||
existing.setOrderId(77L);
|
||||
existing.setInboundCode("RK-CUSTOM-77");
|
||||
existing.setTaskId(700L);
|
||||
when(orders.selectOne(any(Wrapper.class))).thenReturn(null, existing);
|
||||
when(fulfillmentTasks.provision(any(), any())).thenReturn(task(700L));
|
||||
|
||||
long createdId = service.createFromApprovedOrder(command);
|
||||
long replayedId = service.createFromApprovedOrder(command);
|
||||
@@ -50,6 +65,7 @@ class InboundOrderCreationServiceTest {
|
||||
verify(orders, times(1)).insert(inboundCaptor.capture());
|
||||
verify(histories, times(1)).insert(any(InboundStatusHistoryEntity.class));
|
||||
assertThat(inboundCaptor.getValue().getInboundCode()).isEqualTo("RK-CUSTOM-77");
|
||||
assertThat(inboundCaptor.getValue().getTaskId()).isEqualTo(700L);
|
||||
assertThat(inboundCaptor.getValue().getCarrierId()).isNull();
|
||||
assertThat(inboundCaptor.getValue().getTaxMode()).isEqualTo("BUY_DECLARATION");
|
||||
assertThat(inboundCaptor.getValue().getPricingTaxMode()).isEqualTo("TAX_EXCLUDED");
|
||||
@@ -73,7 +89,9 @@ class InboundOrderCreationServiceTest {
|
||||
@Test
|
||||
void shouldConvergeOnConcurrentDuplicateInsertWinner() {
|
||||
InboundOrderEntity winner = inbound(901L, "RK-CUSTOM-77", "UNPRICED", null, null);
|
||||
winner.setTaskId(700L);
|
||||
when(orders.selectOne(any(Wrapper.class))).thenReturn(null, winner);
|
||||
when(fulfillmentTasks.provision(any(), any())).thenReturn(task(700L));
|
||||
doThrow(new org.springframework.dao.DuplicateKeyException("concurrent insert"))
|
||||
.when(orders).insert(any(InboundOrderEntity.class));
|
||||
|
||||
@@ -81,6 +99,20 @@ class InboundOrderCreationServiceTest {
|
||||
verify(histories, never()).insert(any(InboundStatusHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBackfillTaskLinkForInboundCreatedBeforeTaskProvisioning() {
|
||||
InboundOrderEntity existing = inbound(900L, "RK-CUSTOM-77", "UNPRICED", null, null);
|
||||
when(orders.selectOne(any(Wrapper.class))).thenReturn(existing);
|
||||
when(orders.update(any(), any(Wrapper.class))).thenReturn(1);
|
||||
when(fulfillmentTasks.provision(any(), any())).thenReturn(task(700L));
|
||||
|
||||
assertThat(service.createFromApprovedOrder(approvedCommand("RK-CUSTOM-77"))).isEqualTo(900L);
|
||||
|
||||
assertThat(existing.getTaskId()).isEqualTo(700L);
|
||||
verify(orders).update(any(), any(Wrapper.class));
|
||||
verify(orders, never()).insert(any(InboundOrderEntity.class));
|
||||
}
|
||||
|
||||
private static InboundOrderEntity inbound(long id, String code, String feeStatus,
|
||||
Long carrierId, String carrierName) {
|
||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||
@@ -94,6 +126,15 @@ class InboundOrderCreationServiceTest {
|
||||
return inbound;
|
||||
}
|
||||
|
||||
private static FulfillmentTaskEntity task(long id) {
|
||||
FulfillmentTaskEntity task = new FulfillmentTaskEntity();
|
||||
task.setId(id);
|
||||
task.setTenantId(10001L);
|
||||
task.setOrderId(77L);
|
||||
task.setOrderNo("FL-77");
|
||||
return task;
|
||||
}
|
||||
|
||||
private static OrderApprovedInboundCommand approvedCommand(String inboundCode) {
|
||||
return new OrderApprovedInboundCommand(
|
||||
10001L, 77L, "FL-77", inboundCode, 7L, "测试客户", 9L, "义乌仓",
|
||||
|
||||
Reference in New Issue
Block a user