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
@@ -0,0 +1,80 @@
package com.baiyue.lastleg.financefee.state;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baiyue.common.event.BusinessFeeSourceChangedV1;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.common.event.EventTypes;
import com.baiyue.lastleg.entity.LastlegEventOutbox;
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
import com.baiyue.lastleg.financefee.mapper.OverseasFeeSourceMapper;
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.stream.Collectors;
/** A request is not a financial void. Only the matched durable consumer ACK confirms it. */
@Component
@RequiredArgsConstructor
public class OverseasFeeCancellation {
private final LastlegEventOutboxMapper events;
private final OverseasFeeSourceMapper fees;
private final ObjectMapper json;
public static String eventKey(OverseasFeeSource fee, String action) {
return EventTypes.BUSINESS_FEE_SOURCE_CHANGED + ":LASTLEG:" + fee.getSourceType() + ":"
+ fee.getSourceFeeId() + ":" + fee.getSourceVersion() + ":" + action;
}
public boolean requested(OverseasFeeSource fee) { return events.findByEventKey(eventKey(fee, "VOIDED")) != null; }
public List<OverseasFeeSource> describe(List<OverseasFeeSource> values) {
if (values.isEmpty()) return values;
var keys = values.stream().flatMap(value -> java.util.stream.Stream.of(eventKey(value, "VOIDED"),
eventKey(value, value.getSourceVersion() == 1 ? "PUBLISHED" : "ADJUSTED"))).toList();
var byKey = events.selectList(new LambdaQueryWrapper<LastlegEventOutbox>().in(LastlegEventOutbox::getEventKey, keys))
.stream().collect(Collectors.toMap(LastlegEventOutbox::getEventKey, row -> row));
for (var value : values) {
var request = byKey.get(eventKey(value, "VOIDED"));
var publish = byKey.get(eventKey(value, value.getSourceVersion() == 1 ? "PUBLISHED" : "ADJUSTED"));
value.setVoidStatus("VOIDED".equals(value.getSourceStatus()) ? "CONFIRMED" : request == null ? "NOT_REQUESTED"
: "DEAD".equals(request.getStatus()) || publish == null || "DEAD".equals(publish.getStatus())
? "FINANCE_ATTENTION" : "PENDING_FINANCE");
}
return values;
}
public boolean prerequisitePublished(LastlegEventOutbox request) {
if (request.getEventKey() == null || !request.getEventKey().endsWith(":VOIDED")) return true;
String prefix = request.getEventKey().substring(0, request.getEventKey().length() - "VOIDED".length());
return events.selectCount(new LambdaQueryWrapper<LastlegEventOutbox>()
.in(LastlegEventOutbox::getEventKey, List.of(prefix + "PUBLISHED", prefix + "ADJUSTED"))
.eq(LastlegEventOutbox::getStatus, "PUBLISHED")) > 0;
}
/** Invoked inside the same transaction as the lease-owner CAS and delivery journal completion. */
@Transactional(propagation = Propagation.MANDATORY)
public void acknowledge(LastlegEventOutbox event) {
if (event.getEventKey() == null || !event.getEventKey().endsWith(":VOIDED")) return;
try {
var envelope = json.readValue(event.getPayloadJson(), EventEnvelope.class).requireValid();
var payload = json.treeToValue(envelope.payload(), BusinessFeeSourceChangedV1.class);
if (!"VOIDED".equals(payload.action()) || !"LASTLEG".equals(payload.sourceSystem()))
throw new IllegalStateException("费用作废事件与来源不一致");
var source = fees.lockCurrent(Long.parseLong(payload.sourceFeeId()));
if (source == null || !Objects.equals(source.getSourceVersion(), payload.sourceVersion())
|| !Objects.equals(source.getCustomerId(), payload.customerId())
|| !Objects.equals(source.getSourceType(), payload.sourceType())
|| !eventKey(source, "VOIDED").equals(event.getEventKey()) || !"ACTIVE".equals(source.getSourceStatus()))
throw new IllegalStateException("财务已确认的作废来源与当前业务版本不一致");
source.setSourceStatus("VOIDED"); source.setUpdatedAt(LocalDateTime.now(ZoneOffset.UTC));
if (fees.updateById(source) != 1) throw new IllegalStateException("费用作废确认未能落账");
} catch (com.fasterxml.jackson.core.JsonProcessingException error) {
throw new IllegalStateException("费用作废事件无法解析", error);
}
}
}