修复
This commit is contained in:
@@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baiyue.common.
|
||||
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0"); TruckAppointmentEntity e=new TruckAppointmentEntity(); e.setId(IdWorker.getId()); e.setAppointmentNo("TA"+e.getId()); e.setStatus(TruckAppointmentStatus.DRAFT); e.setRevision(0); e.setVersion(0); e.setCreatedAt(LocalDateTime.now()); e.setUpdatedAt(e.getCreatedAt()); copy(e,r); repo.insert(e); record(e,null,e.getStatus().name(),"CREATE",null,uid,name); return e;}
|
||||
@Transactional public TruckAppointmentEntity edit(long id,long uid,String name,SaveRequest r){TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); if(!(e.getStatus()==TruckAppointmentStatus.DRAFT||e.getStatus()==TruckAppointmentStatus.PENDING_DISPATCH||e.getStatus()==TruckAppointmentStatus.RESCHEDULED))throw conflict("当前状态不可编辑"); copy(e,r); if(repo.updateEdit(e,r.expectedVersion())!=1)throw conflict("版本已变化,请刷新后重试"); record(e,e.getStatus().name(),e.getStatus().name(),"EDIT",null,uid,name); return required(id);}
|
||||
@Transactional public TruckAppointmentEntity action(long id,String action,ActionRequest r,String idem,String hash,long uid,String name){TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); TruckAppointmentStatus from=e.getStatus(),to=next(from,action); if("RESCHEDULE".equals(action)){throw conflict("改约请使用专用接口");} if(repo.transition(id,from.name(),to.name(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试"); e.setStatus(to);e.setVersion(e.getVersion()+1);e.setRevision(e.getRevision()+1); record(e,from.name(),to.name(),action,r.reason(),uid,name); operation(e,action,idem,hash); return e;}
|
||||
@Transactional public TruckAppointmentEntity reschedule(long id,LocalDateTime at,ActionRequest r,long uid,String name){TruckAppointmentEntity e=required(id);checkVersion(e,r.expectedVersion()); if(e.getStatus()==TruckAppointmentStatus.COMPLETED||e.getStatus()==TruckAppointmentStatus.CANCELLED)throw conflict("当前状态不可改约"); if(repo.reschedule(id,at,r.reason(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试");e.setStatus(TruckAppointmentStatus.RESCHEDULED);e.setPlannedLoadingAt(at);record(e,e.getStatus().name(),"RESCHEDULED","RESCHEDULE",r.reason(),uid,name);return e;}
|
||||
@Transactional public TruckAppointmentEntity reschedule(long id,LocalDateTime at,ActionRequest r,long uid,String name){TruckAppointmentEntity e=required(id);checkVersion(e,r.expectedVersion()); if(e.getStatus()==TruckAppointmentStatus.COMPLETED||e.getStatus()==TruckAppointmentStatus.CANCELLED)throw conflict("当前状态不可改约"); String from=e.getStatus().name(); if(repo.reschedule(id,at,r.reason(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试");e.setStatus(TruckAppointmentStatus.RESCHEDULED);e.setPlannedLoadingAt(at);e.setVersion(e.getVersion()+1);e.setRevision(e.getRevision()+1);record(e,from,"RESCHEDULED","RESCHEDULE",r.reason(),uid,name);return e;}
|
||||
private TruckAppointmentStatus next(TruckAppointmentStatus s,String a){return switch(a){case"SUBMIT"->s==TruckAppointmentStatus.DRAFT?TruckAppointmentStatus.PENDING_DISPATCH:badStatus();case"DISPATCH"->s==TruckAppointmentStatus.PENDING_DISPATCH||s==TruckAppointmentStatus.RESCHEDULED?TruckAppointmentStatus.DISPATCHED:badStatus();case"PICKUP"->s==TruckAppointmentStatus.DISPATCHED?TruckAppointmentStatus.PICKUP_IN_PROGRESS:badStatus();case"ARRIVE"->s==TruckAppointmentStatus.PICKUP_IN_PROGRESS?TruckAppointmentStatus.ARRIVED_WAREHOUSE:badStatus();case"LOAD"->s==TruckAppointmentStatus.ARRIVED_WAREHOUSE?TruckAppointmentStatus.LOADING:badStatus();case"DEPART"->s==TruckAppointmentStatus.LOADING?TruckAppointmentStatus.DEPARTED:badStatus();case"COMPLETE"->s==TruckAppointmentStatus.DEPARTED?TruckAppointmentStatus.COMPLETED:badStatus();case"EXCEPTION"->s!=TruckAppointmentStatus.COMPLETED&&s!=TruckAppointmentStatus.CANCELLED?TruckAppointmentStatus.EXCEPTION:badStatus();case"CANCEL"->s!=TruckAppointmentStatus.COMPLETED?TruckAppointmentStatus.CANCELLED:badStatus();default->badStatus();};}
|
||||
private TruckAppointmentStatus badStatus(){throw conflict("不允许的状态动作");} private TruckAppointmentEntity required(long id){TruckAppointmentEntity e=repo.find(id);if(e==null)throw new BizException(ResultCode.NOT_FOUND,"拖车预约不存在");return e;} private void checkVersion(TruckAppointmentEntity e,int v){if(e.getVersion()!=v)throw conflict("版本已变化,请刷新后重试");} private static BizException conflict(String m){return new BizException(ResultCode.CONFLICT,m);} private static BizException bad(String m){return new BizException(ResultCode.BAD_REQUEST,m);}
|
||||
private void copy(TruckAppointmentEntity e,SaveRequest r){e.setOutboundOrderId(r.outboundOrderId());e.setOceanBookingId(r.oceanBookingId());e.setContainerBookingId(r.containerBookingId());e.setLoadingPlanId(r.loadingPlanId());e.setWarehouseId(r.warehouseId());e.setCustomerId(r.customerId());e.setCustomerName(r.customerName());e.setCarrierId(r.carrierId());e.setCarrierName(r.carrierName());e.setFleetContact(r.fleetContact());e.setFleetPhone(r.fleetPhone());e.setDriverName(r.driverName());e.setDriverPhone(r.driverPhone());e.setDriverIdCard(r.driverIdCard());e.setVehicleNo(r.vehicleNo());e.setStoreName(r.storeName());e.setStoreContact(r.storeContact());e.setStorePhone(r.storePhone());e.setStoreAddress(r.storeAddress());e.setStoreCity(r.storeCity());e.setStoreZipcode(r.storeZipcode());e.setPackageCount(r.packageCount());e.setGrossWeightKg(r.grossWeightKg());e.setVolumeCbm(r.volumeCbm());e.setPalletCount(r.palletCount());e.setPoNo(r.poNo());e.setPlannedLoadingAt(r.plannedLoadingAt());e.setTransitMinutes(r.transitMinutes());e.setPayableAmount(r.payableAmount());e.setReceivableAmount(r.receivableAmount());e.setCurrency(r.currency()==null?"RMB":r.currency());e.setRemark(r.remark());}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baiyue.firstleg.entity.TruckAppointmentEntity;
|
||||
import com.baiyue.firstleg.mapper.TruckAppointmentMapper;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TruckAppointmentMapperContractTest {
|
||||
@Test void mapsToPhase3TableAndUsesOptimisticGuards() throws Exception {
|
||||
assertThat(TruckAppointmentEntity.class.getAnnotation(TableName.class).value()).isEqualTo("fl_truck_appointment");
|
||||
String page = TruckAppointmentMapper.class.getMethod("page", com.baomidou.mybatisplus.extension.plugins.pagination.Page.class, com.baiyue.firstleg.dto.TruckAppointmentDtos.Query.class).getAnnotation(Select.class).value()[0];
|
||||
assertThat(page).contains("fl_truck_appointment", "appointment_no", "vehicle_no", "status=#{q.status}", "container_booking_id=#{q.containerBookingId}");
|
||||
String transition = TruckAppointmentMapper.class.getMethod("transition", long.class, String.class, String.class, int.class).getAnnotation(Update.class).value();
|
||||
assertThat(transition).contains("status=#{toStatus}", "status=#{fromStatus}", "version=#{version}", "revision=revision+1", "version=version+1");
|
||||
String reschedule = TruckAppointmentMapper.class.getMethod("reschedule", long.class, java.time.LocalDateTime.class, String.class, int.class).getAnnotation(Update.class).value();
|
||||
assertThat(reschedule).contains("status='RESCHEDULED'", "version=#{version}", "revision=revision+1");
|
||||
String edit = TruckAppointmentMapper.class.getMethod("updateEdit", TruckAppointmentEntity.class, int.class).getAnnotation(Update.class).value();
|
||||
assertThat(edit).contains("WHERE id=#{e.id} AND version=#{version}", "version=version+1", "revision=revision+1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.domain.TruckAppointmentStatus;
|
||||
import com.baiyue.firstleg.dto.TruckAppointmentDtos.ActionRequest;
|
||||
import com.baiyue.firstleg.dto.TruckAppointmentDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.entity.TruckAppointmentEntity;
|
||||
import com.baiyue.firstleg.entity.TruckAppointmentHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.TruckAppointmentOperationEntity;
|
||||
import com.baiyue.firstleg.repository.TruckAppointmentRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class TruckAppointmentServiceTest {
|
||||
@Mock TruckAppointmentRepository repository;
|
||||
@InjectMocks TruckAppointmentService service;
|
||||
|
||||
@BeforeEach void setUp() { MockitoAnnotations.openMocks(this); }
|
||||
|
||||
@Test void createRequiresZeroVersionAndInitialisesDraft() {
|
||||
SaveRequest invalid = request(1);
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", invalid)).isInstanceOf(BizException.class);
|
||||
verifyNoInteractions(repository);
|
||||
}
|
||||
|
||||
@Test void actionAdvancesMainLifecycleAndWritesIdempotentOperation() {
|
||||
TruckAppointmentEntity e = entity(TruckAppointmentStatus.DRAFT, 0);
|
||||
when(repository.find(1L)).thenReturn(e);
|
||||
when(repository.transition(1L, "DRAFT", "PENDING_DISPATCH", 0)).thenReturn(1);
|
||||
|
||||
var result = service.action(1L, "submit", new ActionRequest(0, "提交"), "idem-1", "hash-1", 7L, "admin");
|
||||
|
||||
assertThat(result.getStatus()).isEqualTo(TruckAppointmentStatus.PENDING_DISPATCH);
|
||||
assertThat(result.getVersion()).isEqualTo(1);
|
||||
assertThat(result.getRevision()).isEqualTo(1);
|
||||
ArgumentCaptor<TruckAppointmentOperationEntity> operation = ArgumentCaptor.forClass(TruckAppointmentOperationEntity.class);
|
||||
verify(repository).operation(operation.capture());
|
||||
assertThat(operation.getValue().getIdempotencyKey()).isEqualTo("idem-1");
|
||||
verify(repository).history(any(TruckAppointmentHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test void rejectsRescheduleThroughGenericActionAndCompletedReschedule() {
|
||||
TruckAppointmentEntity e = entity(TruckAppointmentStatus.DRAFT, 0);
|
||||
when(repository.find(1L)).thenReturn(e);
|
||||
assertThatThrownBy(() -> service.action(1L, "RESCHEDULE", new ActionRequest(0, null), null, null, 7L, "admin"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("专用接口");
|
||||
e.setStatus(TruckAppointmentStatus.COMPLETED);
|
||||
assertThatThrownBy(() -> service.reschedule(1L, LocalDateTime.now(), new ActionRequest(0, "改约"), 7L, "admin"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不可改约");
|
||||
verify(repository, never()).reschedule(anyLong(), any(), anyString(), anyInt());
|
||||
}
|
||||
|
||||
@Test void rescheduleKeepsFromStatusAndIncrementsOptimisticFields() {
|
||||
TruckAppointmentEntity e = entity(TruckAppointmentStatus.DISPATCHED, 2);
|
||||
when(repository.find(1L)).thenReturn(e);
|
||||
when(repository.reschedule(eq(1L), any(), eq("改约"), eq(2))).thenReturn(1);
|
||||
var at = LocalDateTime.of(2026, 8, 12, 10, 0);
|
||||
var result = service.reschedule(1L, at, new ActionRequest(2, "改约"), 7L, "admin");
|
||||
assertThat(result.getStatus()).isEqualTo(TruckAppointmentStatus.RESCHEDULED);
|
||||
assertThat(result.getVersion()).isEqualTo(3);
|
||||
ArgumentCaptor<TruckAppointmentHistoryEntity> history = ArgumentCaptor.forClass(TruckAppointmentHistoryEntity.class);
|
||||
verify(repository).history(history.capture());
|
||||
assertThat(history.getValue().getFromStatus()).isEqualTo("DISPATCHED");
|
||||
assertThat(history.getValue().getToStatus()).isEqualTo("RESCHEDULED");
|
||||
}
|
||||
|
||||
private static TruckAppointmentEntity entity(TruckAppointmentStatus status, int version) {
|
||||
TruckAppointmentEntity e = new TruckAppointmentEntity(); e.setId(1L); e.setStatus(status); e.setVersion(version); e.setRevision(version); return e;
|
||||
}
|
||||
|
||||
private static SaveRequest request(int expectedVersion) {
|
||||
try {
|
||||
Constructor<?> c = SaveRequest.class.getDeclaredConstructors()[0];
|
||||
Object[] args = new Object[c.getParameterCount()]; args[args.length - 1] = expectedVersion;
|
||||
return (SaveRequest) c.newInstance(args);
|
||||
} catch (ReflectiveOperationException ex) { throw new AssertionError(ex); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user