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) { 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; LastlegEventOutbox existing = mapper.findByEventKey(eventKey); 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; WalletCommandRequestedV1 value = new WalletCommandRequestedV1(operation, customerId, businessType, businessNo, amount, remark, idempotencyKey, Instant.now(), originalLedgerId); JsonNode payload = objectMapper.valueToTree(value); EventEnvelope envelope = new EventEnvelope(EventIdentifiers.newUuid(), EventTypes.WALLET_COMMAND_REQUESTED, WalletCommandRequestedV1.VERSION, Instant.now(), 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); } } }