You've already forked FrameTour-BE
feat(profit-share): 实现分账管理V2版本功能
- 新增分账规则的创建、查询、更新、启用、禁用和删除接口 - 新增分账记录的查询接口,支持按景区、订单ID等多种方式查询 - 新增手动触发分账和计算分账结果的功能接口 - 新增获取支持类型的接口,方便前端展示和选择- 集成分账服务Feign客户端,实现与zt-profitshare微服务通信 - 添加Kafka消息配置,支持分账和退款消息的发送 - 完善DTO结构定义,包括规则、记录、明细及消息相关实体类 - 实现集成服务层,封装对分账服务的操作并提供fallback机制 - 控制器层增加参数校验和异常处理逻辑,提高系统健壮性- 所有接口均遵循RESTful设计规范,并提供详细的日志记录
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user