feat(profit-share): 实现分账管理V2版本功能

- 新增分账规则的创建、查询、更新、启用、禁用和删除接口
- 新增分账记录的查询接口,支持按景区、订单ID等多种方式查询
- 新增手动触发分账和计算分账结果的功能接口
- 新增获取支持类型的接口,方便前端展示和选择- 集成分账服务Feign客户端,实现与zt-profitshare微服务通信
- 添加Kafka消息配置,支持分账和退款消息的发送
- 完善DTO结构定义,包括规则、记录、明细及消息相关实体类
- 实现集成服务层,封装对分账服务的操作并提供fallback机制
- 控制器层增加参数校验和异常处理逻辑,提高系统健壮性- 所有接口均遵循RESTful设计规范,并提供详细的日志记录
This commit is contained in:
2025-10-13 20:30:46 +08:00
parent be375067ce
commit bdeb41bead
20 changed files with 2053 additions and 1 deletions

View File

@@ -0,0 +1,224 @@
package com.ycwl.basic.integration.profitshare.service;
import com.ycwl.basic.integration.common.exception.IntegrationException;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
import com.ycwl.basic.integration.profitshare.client.ProfitShareClient;
import com.ycwl.basic.integration.profitshare.dto.CalculateResultVO;
import com.ycwl.basic.integration.profitshare.dto.CalculateShareRequest;
import com.ycwl.basic.integration.profitshare.dto.ManualShareRequest;
import com.ycwl.basic.integration.profitshare.dto.TypesVO;
import com.ycwl.basic.integration.profitshare.dto.record.RecordDetailVO;
import com.ycwl.basic.integration.profitshare.dto.record.RecordVO;
import com.ycwl.basic.integration.profitshare.dto.rule.CreateRuleRequest;
import com.ycwl.basic.integration.profitshare.dto.rule.RuleVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 分账集成服务
*
* @author Claude Code
* @date 2025-01-11
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ProfitShareIntegrationService {
private final ProfitShareClient profitShareClient;
private final IntegrationFallbackService fallbackService;
private static final String SERVICE_NAME = "zt-profitshare";
// ==================== 规则管理 ====================
/**
* 创建分账规则(直接操作,无fallback)
*/
public RuleVO createRule(CreateRuleRequest request) {
log.debug("创建分账规则, scenicId: {}, ruleName: {}", request.getScenicId(), request.getRuleName());
CommonResponse<RuleVO> response = profitShareClient.createRule(request);
return handleResponse(response, "创建分账规则失败");
}
/**
* 查询分账规则列表(带fallback)
*/
public PageResponse<RuleVO> listRules(Long scenicId, String status, String ruleType, Integer page, Integer pageSize) {
log.debug("查询分账规则列表, scenicId: {}, status: {}, ruleType: {}, page: {}, pageSize: {}",
scenicId, status, ruleType, page, pageSize);
return fallbackService.executeWithFallback(
SERVICE_NAME,
String.format("rules:list:%s:%s:%s:%d:%d", scenicId, status, ruleType, page, pageSize),
() -> {
CommonResponse<PageResponse<RuleVO>> response = profitShareClient.listRules(scenicId, status, ruleType, page, pageSize);
return handleResponse(response, "查询分账规则列表失败");
},
PageResponse.class
);
}
/**
* 获取分账规则详情(带fallback)
*/
public RuleVO getRule(Long ruleId) {
log.debug("获取分账规则详情, ruleId: {}", ruleId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"rule:" + ruleId,
() -> {
CommonResponse<RuleVO> response = profitShareClient.getRule(ruleId);
return handleResponse(response, "获取分账规则详情失败");
},
RuleVO.class
);
}
/**
* 更新分账规则(直接操作,无fallback)
*/
public RuleVO updateRule(Long ruleId, CreateRuleRequest request) {
log.debug("更新分账规则, ruleId: {}", ruleId);
CommonResponse<RuleVO> response = profitShareClient.updateRule(ruleId, request);
return handleResponse(response, "更新分账规则失败");
}
/**
* 删除分账规则(直接操作,无fallback)
*/
public void deleteRule(Long ruleId) {
log.debug("删除分账规则, ruleId: {}", ruleId);
CommonResponse<Void> response = profitShareClient.deleteRule(ruleId);
handleResponse(response, "删除分账规则失败");
}
/**
* 启用规则(直接操作,无fallback)
*/
public void enableRule(Long ruleId) {
log.debug("启用分账规则, ruleId: {}", ruleId);
CommonResponse<Void> response = profitShareClient.enableRule(ruleId);
handleResponse(response, "启用分账规则失败");
}
/**
* 禁用规则(直接操作,无fallback)
*/
public void disableRule(Long ruleId) {
log.debug("禁用分账规则, ruleId: {}", ruleId);
CommonResponse<Void> response = profitShareClient.disableRule(ruleId);
handleResponse(response, "禁用分账规则失败");
}
// ==================== 分账记录查询 ====================
/**
* 查询景区分账记录(带fallback)
*/
public PageResponse<RecordVO> getRecordsByScenic(Long scenicId, Integer page, Integer pageSize) {
log.debug("查询景区分账记录, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
return fallbackService.executeWithFallback(
SERVICE_NAME,
String.format("records:scenic:%d:%d:%d", scenicId, page, pageSize),
() -> {
CommonResponse<PageResponse<RecordVO>> response = profitShareClient.getRecordsByScenic(scenicId, page, pageSize);
return handleResponse(response, "查询景区分账记录失败");
},
PageResponse.class
);
}
/**
* 查询分账记录详情(带fallback)
*/
public RecordDetailVO getRecordById(Long recordId) {
log.debug("查询分账记录详情, recordId: {}", recordId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"record:" + recordId,
() -> {
CommonResponse<RecordDetailVO> response = profitShareClient.getRecordById(recordId);
return handleResponse(response, "查询分账记录详情失败");
},
RecordDetailVO.class
);
}
/**
* 按订单ID查询分账记录(带fallback)
*/
public RecordDetailVO getRecordByOrderId(String orderId) {
log.debug("按订单ID查询分账记录, orderId: {}", orderId);
return fallbackService.executeWithFallback(
SERVICE_NAME,
"record:order:" + orderId,
() -> {
CommonResponse<RecordDetailVO> response = profitShareClient.getRecordByOrderId(orderId);
return handleResponse(response, "按订单ID查询分账记录失败");
},
RecordDetailVO.class
);
}
// ==================== 分账操作 ====================
/**
* 手动触发分账(直接操作,无fallback)
*/
public void manualShare(String orderId) {
log.debug("手动触发分账, orderId: {}", orderId);
ManualShareRequest request = ManualShareRequest.builder()
.orderId(orderId)
.build();
CommonResponse<Void> response = profitShareClient.manualShare(request);
handleResponse(response, "手动触发分账失败");
}
/**
* 计算分账金额(不执行)(带fallback)
*/
public CalculateResultVO calculateShare(CalculateShareRequest request) {
log.debug("计算分账金额, scenicId: {}, totalAmount: {}", request.getScenicId(), request.getTotalAmount());
return fallbackService.executeWithFallback(
SERVICE_NAME,
String.format("calculate:%d:%.2f", request.getScenicId(), request.getTotalAmount()),
() -> {
CommonResponse<CalculateResultVO> response = profitShareClient.calculateShare(request);
return handleResponse(response, "计算分账金额失败");
},
CalculateResultVO.class
);
}
/**
* 获取支持的类型(带fallback)
*/
public TypesVO getSupportedTypes() {
log.debug("获取支持的类型");
return fallbackService.executeWithFallback(
SERVICE_NAME,
"types",
() -> {
CommonResponse<TypesVO> response = profitShareClient.getSupportedTypes();
return handleResponse(response, "获取支持的类型失败");
},
TypesVO.class
);
}
// ==================== 私有方法 ====================
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
if (response == null || !response.isSuccess()) {
String msg = response != null && response.getMessage() != null
? response.getMessage()
: errorMessage;
Integer code = response != null ? response.getCode() : 5000;
throw new IntegrationException(code, msg, SERVICE_NAME);
}
return response.getData();
}
}

View File

@@ -0,0 +1,131 @@
package com.ycwl.basic.integration.profitshare.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.integration.kafka.config.KafkaIntegrationProperties;
import com.ycwl.basic.integration.profitshare.dto.message.OrderMessage;
import com.ycwl.basic.integration.profitshare.dto.message.RefundMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
/**
* 分账Kafka消息生产者
*
* @author Claude Code
* @date 2025-01-11
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(name = "kafka.enabled", havingValue = "true")
public class ProfitShareKafkaProducer {
public static final String DEFAULT_PROFITSHARE_TOPIC = "zt-profitshare";
public static final String DEFAULT_REFUND_TOPIC = "zt-refund";
private final KafkaTemplate<String, String> kafkaTemplate;
private final ObjectMapper objectMapper;
private final KafkaIntegrationProperties kafkaProps;
/**
* 发送分账消息(订单支付成功后调用)
*/
public void sendProfitShareMessage(OrderMessage message) {
validate(message);
String topic = kafkaProps != null && StringUtils.isNotBlank(kafkaProps.getProfitShareTopic())
? kafkaProps.getProfitShareTopic()
: DEFAULT_PROFITSHARE_TOPIC;
String key = message.getOrderId();
String payload = toJson(message);
log.info("[PROFIT-SHARE] producing to topic={}, key={}, orderId={}, scenicId={}, amount={}",
topic, key, message.getOrderId(), message.getScenicId(), message.getTotalAmount());
kafkaTemplate.send(topic, key, payload).whenComplete((metadata, ex) -> {
if (ex != null) {
log.error("[PROFIT-SHARE] produce failed: orderId={}, error={}", message.getOrderId(), ex.getMessage(), ex);
} else if (metadata != null) {
log.info("[PROFIT-SHARE] produced: orderId={}, partition={}, offset={}",
message.getOrderId(), metadata.getRecordMetadata().partition(), metadata.getRecordMetadata().offset());
}
});
}
/**
* 发送退款消息(订单退款成功后调用)
*/
public void sendRefundMessage(RefundMessage message) {
validateRefund(message);
String topic = kafkaProps != null && StringUtils.isNotBlank(kafkaProps.getRefundTopic())
? kafkaProps.getRefundTopic()
: DEFAULT_REFUND_TOPIC;
String key = message.getOrderId();
String payload = toJson(message);
log.info("[REFUND] producing to topic={}, key={}, orderId={}, scenicId={}, amount={}",
topic, key, message.getOrderId(), message.getScenicId(), message.getRefundAmount());
kafkaTemplate.send(topic, key, payload).whenComplete((metadata, ex) -> {
if (ex != null) {
log.error("[REFUND] produce failed: orderId={}, error={}", message.getOrderId(), ex.getMessage(), ex);
} else if (metadata != null) {
log.info("[REFUND] produced: orderId={}, partition={}, offset={}",
message.getOrderId(), metadata.getRecordMetadata().partition(), metadata.getRecordMetadata().offset());
}
});
}
private void validate(OrderMessage msg) {
if (msg == null) {
throw new IllegalArgumentException("OrderMessage is null");
}
if (StringUtils.isBlank(msg.getOrderId())) {
throw new IllegalArgumentException("orderId is required");
}
if (msg.getScenicId() == null || msg.getScenicId() <= 0) {
throw new IllegalArgumentException("scenicId is required and must be positive");
}
if (msg.getTotalAmount() == null || msg.getTotalAmount() <= 0) {
throw new IllegalArgumentException("totalAmount is required and must be positive");
}
if (StringUtils.isBlank(msg.getPaymentSystem())) {
throw new IllegalArgumentException("paymentSystem is required");
}
if (StringUtils.isBlank(msg.getPaymentOrderId())) {
throw new IllegalArgumentException("paymentOrderId is required");
}
}
private void validateRefund(RefundMessage msg) {
if (msg == null) {
throw new IllegalArgumentException("RefundMessage is null");
}
if (StringUtils.isBlank(msg.getOrderId())) {
throw new IllegalArgumentException("orderId is required");
}
if (msg.getScenicId() == null || msg.getScenicId() <= 0) {
throw new IllegalArgumentException("scenicId is required and must be positive");
}
if (msg.getRefundAmount() == null || msg.getRefundAmount() <= 0) {
throw new IllegalArgumentException("refundAmount is required and must be positive");
}
if (StringUtils.isBlank(msg.getPaymentSystem())) {
throw new IllegalArgumentException("paymentSystem is required");
}
if (StringUtils.isBlank(msg.getRefundOrderId())) {
throw new IllegalArgumentException("refundOrderId is required");
}
}
private String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("failed to serialize message", e);
}
}
}