Files
baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceCommandService.java
T

194 lines
9.2 KiB
Java

package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FinanceFeeSourceCommandDtos.CommandResult;
import com.baiyue.firstleg.dto.FinanceFeeSourceCommandDtos.VoidRequest;
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
import com.baiyue.firstleg.entity.FinanceFeeSourceCommandEntity;
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import com.baiyue.firstleg.repository.FeeItemRepository;
import com.baiyue.firstleg.repository.FinanceFeeSourceCommandRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.HexFormat;
/** Finance 仅发起命令;业务状态与费用事件始终由 Firstleg 原子产生。 */
@Service
@RequiredArgsConstructor
public class FinanceFeeSourceCommandService {
private static final String ACTION_VOID = "VOID";
private final FinanceFeeSourceCommandRepository commands;
private final FeeItemRepository inboundFees;
private final FbaPricingRepository pricing;
private final ContainerFeeDetailRepository containerFees;
private final ContainerFeeDetailService containerFeeService;
private final FinanceFeeSourceOutboxService financeOutbox;
private final ObjectMapper objectMapper;
@Transactional
public CommandResult voidSource(String idempotencyKey, String clientHash, VoidRequest request) {
validateKey(idempotencyKey);
validateIdentity(request);
String requestHash = hash(request);
if (!StringUtils.hasText(clientHash) || !requestHash.equalsIgnoreCase(clientHash.trim())) {
throw bad("X-Request-Hash与费用作废请求不一致");
}
FinanceFeeSourceCommandEntity command = claim(idempotencyKey.trim(), requestHash, request);
if ("SUCCEEDED".equals(command.getStatus())) {
return result(request, command.getResultStatus(), true);
}
if ("INBOUND_FEE".equals(request.sourceType())) voidInbound(request);
else voidContainer(request);
if (commands.complete(command.getId(), "VOIDED", LocalDateTime.now()) != 1) {
throw conflict("费用作废幂等记录状态已变化");
}
return result(request, "VOIDED", false);
}
private FinanceFeeSourceCommandEntity claim(String key, String requestHash, VoidRequest request) {
FinanceFeeSourceCommandEntity value = new FinanceFeeSourceCommandEntity();
value.setCustomerId(request.customerId());
value.setOperatorId(request.operatorId());
value.setOperatorName(request.operatorName().trim());
value.setAction(ACTION_VOID);
value.setIdempotencyKey(key);
value.setRequestHash(requestHash);
value.setSourceType(request.sourceType());
value.setSourceFeeId(request.sourceFeeId());
value.setSourceVersion(request.sourceVersion());
value.setInboundId(request.inboundId());
value.setContainerId(request.containerId());
value.setReason(request.reason().trim());
value.setStatus("PROCESSING");
value.setCreatedAt(LocalDateTime.now());
try {
commands.insert(value);
return value;
} catch (DuplicateKeyException duplicate) {
FinanceFeeSourceCommandEntity existing = commands.find(request.customerId(),
request.operatorId(), ACTION_VOID, key);
if (existing == null || !requestHash.equals(existing.getRequestHash())) {
throw conflict("Idempotency-Key已用于其他费用作废请求");
}
if (!"SUCCEEDED".equals(existing.getStatus())) {
throw conflict("相同费用作废请求正在处理中");
}
return existing;
}
}
private void voidInbound(VoidRequest request) {
long inboundId = positive(request.inboundId(), "入库单ID");
if (request.containerId() != null) throw bad("入库费用不能携带柜次ID");
InboundFeeDetailEntity detail = inboundFees.findSourceVersion(inboundId,
request.sourceFeeId(), request.sourceVersion());
if (detail == null || !Long.valueOf(request.customerId()).equals(detail.getTenantId())) {
throw new BizException(ResultCode.NOT_FOUND, "指定客户的入库费用来源版本不存在");
}
if ("VOIDED".equals(detail.getPublicationStatus())) return;
if (!"APPROVED".equals(detail.getReviewStatus())
|| !"PUBLISHED".equals(detail.getPublicationStatus())) {
throw conflict("只有当前已审核发布的入库费用来源可以作废");
}
LocalDateTime now = LocalDateTime.now();
if ("FBA_PRICING".equals(detail.getSourceType())) {
if (pricing.voidSentFee(request.customerId(), inboundId, request.sourceFeeId(),
request.sourceVersion(), now) != 1
|| pricing.markInboundFeeVoided(request.customerId(), inboundId) != 1) {
throw conflict("入库计价费用状态或版本已变化");
}
} else if (!"MANUAL".equals(detail.getSourceType())) {
throw conflict("该入库费用来源类型不支持作废");
}
if (inboundFees.voidApproved(request.customerId(), inboundId, detail.getId(),
request.sourceVersion(), request.operatorId()) != 1) {
throw conflict("入库费用状态或版本已变化");
}
financeOutbox.publishInbound(FinanceFeeSourceOutboxService.VOIDED,
request.customerId(), inboundId, request.sourceFeeId(), request.sourceVersion(), now);
}
private void voidContainer(VoidRequest request) {
long containerBookingId = positive(request.containerId(), "柜次ID");
if (request.inboundId() != null) throw bad("柜次费用不能携带入库单ID");
ContainerFeeDetailEntity detail = containerFees.findByContainerBookingId(
containerBookingId, request.sourceFeeId());
if (detail == null || !Long.valueOf(request.customerId()).equals(detail.getTenantId())) {
throw new BizException(ResultCode.NOT_FOUND, "指定客户的柜次费用来源版本不存在");
}
if (!Integer.valueOf(request.sourceVersion()).equals(detail.getSourceVersion())) {
throw conflict("柜次费用来源版本已变化");
}
if ("VOIDED".equals(detail.getSourceStatus())) return;
containerFeeService.voidApproved(request.customerId(), detail.getLoadingPlanId(),
request.sourceFeeId(), request.sourceVersion(), request.operatorId());
}
private String hash(VoidRequest request) {
try {
return sha256(objectMapper.writeValueAsString(request));
} catch (JsonProcessingException exception) {
throw new BizException(ResultCode.INTERNAL_ERROR, "费用作废请求序列化失败");
}
}
private static void validateIdentity(VoidRequest request) {
if (request == null || request.sourceFeeId() <= 0 || request.sourceVersion() <= 0
|| request.customerId() <= 0 || request.operatorId() <= 0
|| !StringUtils.hasText(request.operatorName()) || !StringUtils.hasText(request.reason())) {
throw bad("费用作废请求无效");
}
if (!"INBOUND_FEE".equals(request.sourceType())
&& !"CONTAINER_FEE".equals(request.sourceType())) {
throw bad("费用来源类型无效");
}
}
private static void validateKey(String key) {
if (!StringUtils.hasText(key) || key.trim().length() > 128) {
throw bad("Idempotency-Key不能为空且不能超过128个字符");
}
}
private static long positive(Long value, String label) {
if (value == null || value <= 0) throw bad(label + "无效");
return value;
}
private static CommandResult result(VoidRequest request, String status, boolean replayed) {
return new CommandResult(request.sourceType(), request.sourceFeeId(),
request.sourceVersion(), status, replayed);
}
private static String sha256(String value) {
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(value.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 unavailable", exception);
}
}
private static BizException bad(String message) {
return new BizException(ResultCode.BAD_REQUEST, message);
}
private static BizException conflict(String message) {
return new BizException(ResultCode.CONFLICT, message);
}
}