feat(fleet): enforce reviewed trips and original fee coverage for dispatch

This commit is contained in:
2026-10-03 04:18:52 +08:00
parent c50e9f97d1
commit 5e683da3b9
162 changed files with 9642 additions and 218 deletions
@@ -54,6 +54,7 @@ public class OverseasFeeOutboxRelay implements EventDeliveryProducer {
failures, next, "投递租约超时,消费结果未知"));
continue; // Recovery always waits for the new backoff window; never sends in this pass.
}
if (!repository.prerequisitePublished(candidate)) continue;
String worker = UUID.randomUUID().toString();
String type = Stream.FEE.type(candidate.getEventType());
if (!journal.begin(worker, Stream.FEE.service, type, String.valueOf(candidate.getId()),
@@ -11,6 +11,7 @@ import com.baiyue.lastleg.financefee.entity.OverseasFeeCommand;
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
import com.baiyue.lastleg.financefee.mapper.OverseasFeeCommandMapper;
import com.baiyue.lastleg.financefee.mapper.OverseasFeeSourceMapper;
import com.baiyue.lastleg.financefee.state.OverseasFeeCancellation;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
@@ -41,31 +42,43 @@ public class OverseasFeeSourceService {
private final OverseasFeeOutboxService outbox;
private final OverseasFeeCurrencyConverter currencyConverter;
private final ObjectMapper objectMapper;
private final OverseasFeeCancellation cancellation;
public List<OverseasFeeSource> list(String customerId, String businessType, String businessNo,
Long containerBookingId) {
return mapper.selectList(new LambdaQueryWrapper<OverseasFeeSource>()
return cancellation.describe(mapper.selectList(new LambdaQueryWrapper<OverseasFeeSource>()
.eq(text(customerId) != null, OverseasFeeSource::getCustomerId, text(customerId))
.eq(text(businessType) != null, OverseasFeeSource::getBusinessType, text(businessType))
.like(text(businessNo) != null, OverseasFeeSource::getBusinessNo, text(businessNo))
.eq(containerBookingId != null, OverseasFeeSource::getContainerBookingId, containerBookingId)
.inSql(OverseasFeeSource::getId, "SELECT MAX(v.id) FROM ll_finance_fee_source v GROUP BY v.source_fee_id")
.orderByDesc(OverseasFeeSource::getCreatedAt).orderByDesc(OverseasFeeSource::getId));
.orderByDesc(OverseasFeeSource::getCreatedAt).orderByDesc(OverseasFeeSource::getId)));
}
public List<OverseasFeeSource> listForBusiness(String customerId, String businessType, long businessId) {
return mapper.selectList(new LambdaQueryWrapper<OverseasFeeSource>()
return cancellation.describe(mapper.selectList(new LambdaQueryWrapper<OverseasFeeSource>()
.eq(OverseasFeeSource::getCustomerId, customerId)
.eq(OverseasFeeSource::getBusinessType, businessType)
.eq(OverseasFeeSource::getBusinessId, businessId)
.inSql(OverseasFeeSource::getId, "SELECT MAX(v.id) FROM ll_finance_fee_source v GROUP BY v.source_fee_id")
.orderByAsc(OverseasFeeSource::getCreatedAt).orderByAsc(OverseasFeeSource::getId));
.orderByAsc(OverseasFeeSource::getCreatedAt).orderByAsc(OverseasFeeSource::getId)));
}
public List<ContainerCustomerOption> listContainerCustomers(long containerBookingId) {
return businessGuard.listContainerCustomers(containerBookingId);
}
public void requireNoActiveDeliveryReceivables(String customerId, List<String> inboundCodes) {
if (inboundCodes.isEmpty()) return;
long existing = mapper.selectCount(new LambdaQueryWrapper<OverseasFeeSource>()
.eq(OverseasFeeSource::getCustomerId, customerId).eq(OverseasFeeSource::getBusinessType, "DELIVERY_ORDER")
.eq(OverseasFeeSource::getDirection, "RECEIVABLE").eq(OverseasFeeSource::getFeeItemCode, "DELIVERY_FEE")
.in(OverseasFeeSource::getInboundCode, inboundCodes).ne(OverseasFeeSource::getSourceStatus, "VOIDED")
.and(state -> state.eq(OverseasFeeSource::getReviewStatus, "APPROVED").or().gt(OverseasFeeSource::getSourceVersion, 1))
.inSql(OverseasFeeSource::getId, "SELECT MAX(v.id) FROM ll_finance_fee_source v GROUP BY v.source_fee_id"));
if (existing > 0) throw conflict("外接货已有派送应收尚未确认作废,请先完成原费用处理再重新转派送");
}
@Transactional
public OverseasFeeSource create(String operatorId, String key, SaveRequest request) {
OverseasFeeCommand replay = replay(operatorId, "CREATE", key, request);
@@ -125,11 +138,12 @@ public class OverseasFeeSourceService {
return value;
}
@Transactional
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
public OverseasFeeSource adjust(long sourceFeeId, String operatorId, AdjustRequest request) {
OverseasFeeCommand replay = replay(operatorId, "ADJUST", request.idempotencyKey(), request);
if (replay != null) return current(replay.getSourceFeeId());
OverseasFeeSource old = lock(sourceFeeId);
if (cancellation.requested(old)) throw conflict("该费用已申请作废,请先处理财务结果");
if (!"APPROVED".equals(old.getReviewStatus()) || !"ACTIVE".equals(old.getSourceStatus())
|| old.getSourceVersion() != request.expectedVersion()) throw conflict("只有当前已发布版本可以调整");
validate(request.fee()); businessGuard.requireOwned(request.fee());
@@ -157,10 +171,15 @@ public class OverseasFeeSourceService {
OverseasFeeSource value = lock(sourceFeeId);
if (!"APPROVED".equals(value.getReviewStatus()) || !"ACTIVE".equals(value.getSourceStatus())
|| value.getSourceVersion() != request.expectedVersion()) throw conflict("只有当前已发布版本可以作废");
value.setSourceStatus("VOIDED"); value.setReviewReason(request.reason().trim());
if (cancellation.requested(value)) return cancellation.describe(List.of(value)).get(0);
value.setReviewReason(request.reason().trim());
value.setUpdatedBy(operatorId); value.setUpdatedAt(now()); mapper.updateById(value);
outbox.publish(value, "VOIDED");
var pending = new OverseasFeeSource();
org.springframework.beans.BeanUtils.copyProperties(value, pending);
pending.setSourceStatus("VOIDED");
outbox.publish(pending, "VOIDED");
command(operatorId, "VOID", request.idempotencyKey(), request, sourceFeeId, value.getSourceVersion());
value.setVoidStatus("PENDING_FINANCE");
return value;
}
@@ -233,7 +252,7 @@ public class OverseasFeeSourceService {
private OverseasFeeSource current(long sourceFeeId) {
OverseasFeeSource value = mapper.selectOne(new LambdaQueryWrapper<OverseasFeeSource>()
.eq(OverseasFeeSource::getSourceFeeId, sourceFeeId).orderByDesc(OverseasFeeSource::getSourceVersion).last("LIMIT 1"));
if (value == null) throw notFound(); return value;
if (value == null) throw notFound(); return cancellation.describe(List.of(value)).get(0);
}
private OverseasFeeSource lock(long sourceFeeId) { OverseasFeeSource value = mapper.lockCurrent(sourceFeeId); if (value == null) throw notFound(); return value; }
private OverseasFeeCommand replay(String operator, String action, String key, Object request) {