2026-09-05 22:26:25 +08:00
|
|
|
package com.baiyue.lastleg.wallet;
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
|
|
|
|
import com.baiyue.common.event.EventEnvelope;
|
|
|
|
|
import com.baiyue.common.event.EventIdentifiers;
|
|
|
|
|
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
|
|
|
|
import com.baiyue.common.event.EventPayloadHash;
|
|
|
|
|
import com.baiyue.common.event.EventTypes;
|
|
|
|
|
import com.baiyue.common.event.WalletCommandRequestedV1;
|
|
|
|
|
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
|
|
|
|
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
|
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.time.ZoneOffset;
|
|
|
|
|
|
|
|
|
|
@Service
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
public class WalletCommandOutboxService {
|
|
|
|
|
private final LastlegEventOutboxMapper mapper;
|
|
|
|
|
private final ObjectMapper objectMapper;
|
|
|
|
|
|
|
|
|
|
public String publish(String operation, long customerId, String businessType, String businessNo,
|
|
|
|
|
BigDecimal amount, String remark, long aggregateVersion) {
|
2026-10-10 02:19:38 +08:00
|
|
|
if ("REFUND".equals(operation)) throw new IllegalArgumentException("退款必须关联原扣款流水");
|
|
|
|
|
return publishCommand(operation, customerId, businessType, businessNo, amount, remark, aggregateVersion, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String publishRefund(long customerId, String businessType, String refundNo, BigDecimal amount,
|
|
|
|
|
String originalLedgerId, String remark, long aggregateVersion) {
|
|
|
|
|
if (originalLedgerId == null || !originalLedgerId.matches("[1-9][0-9]*")) {
|
|
|
|
|
throw new IllegalArgumentException("退款原扣款流水无效");
|
|
|
|
|
}
|
|
|
|
|
return publishCommand("REFUND", customerId, businessType, refundNo, amount, remark, aggregateVersion, originalLedgerId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String publishCommand(String operation, long customerId, String businessType, String businessNo,
|
|
|
|
|
BigDecimal amount, String remark, long aggregateVersion, String originalLedgerId) {
|
|
|
|
|
String eventKey = "WALLET_COMMAND:" + customerId + ':' + businessType + ':' + businessNo + ':' + operation + ':' + aggregateVersion;
|
2026-09-05 22:26:25 +08:00
|
|
|
LastlegEventOutbox existing = mapper.findByEventKey(eventKey);
|
2026-10-10 02:19:38 +08:00
|
|
|
if (existing != null) {
|
|
|
|
|
try {
|
|
|
|
|
EventEnvelope saved = objectMapper.readValue(existing.getPayloadJson(), EventEnvelope.class);
|
|
|
|
|
WalletCommandRequestedV1 command = objectMapper.treeToValue(saved.payload(), WalletCommandRequestedV1.class);
|
|
|
|
|
if (command.customerId() != customerId || !operation.equals(command.operation())
|
|
|
|
|
|| !businessType.equals(command.businessType()) || !businessNo.equals(command.businessNo())
|
|
|
|
|
|| !java.util.Objects.equals(remark, command.remark())
|
|
|
|
|
|| !java.util.Objects.equals(originalLedgerId, command.originalLedgerId())
|
|
|
|
|
|| (amount == null ? command.amount() != null : command.amount() == null || amount.compareTo(command.amount()) != 0)) {
|
|
|
|
|
throw new IllegalStateException("同一资金请求对应不同费用事实");
|
|
|
|
|
}
|
|
|
|
|
return saved.eventId();
|
|
|
|
|
} catch (java.io.IOException exception) {
|
|
|
|
|
throw new IllegalStateException("资金指令事件快照损坏", exception);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
String idempotencyKey = customerId + ":" + businessType + ':' + businessNo + ':' + operation + ':' + aggregateVersion;
|
2026-09-05 22:26:25 +08:00
|
|
|
WalletCommandRequestedV1 value = new WalletCommandRequestedV1(operation, customerId, businessType,
|
2026-10-10 02:19:38 +08:00
|
|
|
businessNo, amount, remark, idempotencyKey, Instant.now(), originalLedgerId);
|
2026-09-05 22:26:25 +08:00
|
|
|
JsonNode payload = objectMapper.valueToTree(value);
|
|
|
|
|
EventEnvelope envelope = new EventEnvelope(EventIdentifiers.newUuid(), EventTypes.WALLET_COMMAND_REQUESTED,
|
2026-09-29 18:47:37 +08:00
|
|
|
WalletCommandRequestedV1.VERSION, Instant.now(),
|
2026-09-05 22:26:25 +08:00
|
|
|
String.valueOf(customerId), businessType, businessNo, aggregateVersion, null,
|
|
|
|
|
EventIdentifiers.newUuid(), eventKey, null, payload,
|
|
|
|
|
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload))).requireValid();
|
|
|
|
|
LastlegEventOutbox row = new LastlegEventOutbox();
|
|
|
|
|
row.setId(IdWorker.getId()); row.setCustomerId(String.valueOf(customerId));
|
|
|
|
|
row.setAggregateType(businessType); row.setAggregateId(businessNo);
|
|
|
|
|
row.setEventType(envelope.eventType()); row.setEventVersion(envelope.eventVersion());
|
|
|
|
|
row.setEventKey(eventKey); row.setPayloadJson(json(envelope)); row.setStatus("PENDING");
|
|
|
|
|
row.setRetryCount(0); row.setCreatedAt(LocalDateTime.now(ZoneOffset.UTC));
|
|
|
|
|
mapper.insert(row);
|
|
|
|
|
return envelope.eventId();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String json(Object value) {
|
|
|
|
|
try { return objectMapper.writeValueAsString(value); }
|
|
|
|
|
catch (Exception exception) { throw new IllegalStateException("资金指令事件序列化失败", exception); }
|
|
|
|
|
}
|
|
|
|
|
}
|