Merge branch 'profitshare'

This commit is contained in:
2025-12-16 10:45:30 +08:00
20 changed files with 2054 additions and 1 deletions

View File

@@ -0,0 +1,257 @@
package com.ycwl.basic.controller.pc;
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 com.ycwl.basic.integration.profitshare.service.ProfitShareIntegrationService;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 分账管理 V2 版本控制器 - 基于 zt-profitshare 集成服务
*
* @author Claude Code
* @date 2025-01-11
*/
@Slf4j
@RestController
@RequestMapping("/api/profit-share/v2")
@RequiredArgsConstructor
public class ProfitShareV2Controller {
private final ProfitShareIntegrationService profitShareIntegrationService;
// ========== 分账规则管理 ==========
/**
* 创建分账规则
*/
@PostMapping("/rules")
public ApiResponse<RuleVO> createRule(@Valid @RequestBody CreateRuleRequest request) {
log.info("创建分账规则, scenicId: {}, ruleName: {}, ruleType: {}",
request.getScenicId(), request.getRuleName(), request.getRuleType());
try {
RuleVO rule = profitShareIntegrationService.createRule(request);
return ApiResponse.success(rule);
} catch (Exception e) {
log.error("创建分账规则失败", e);
return ApiResponse.fail("创建分账规则失败: " + e.getMessage());
}
}
/**
* 查询分账规则列表
*/
@GetMapping("/rules")
public ApiResponse<PageResponse<RuleVO>> listRules(
@RequestParam(required = false) Long scenicId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String ruleType,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("查询分账规则列表, scenicId: {}, status: {}, ruleType: {}, page: {}, pageSize: {}",
scenicId, status, ruleType, page, pageSize);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<RuleVO> response = profitShareIntegrationService.listRules(scenicId, status, ruleType, page, pageSize);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("查询分账规则列表失败", e);
return ApiResponse.fail("查询分账规则列表失败: " + e.getMessage());
}
}
/**
* 获取分账规则详情
*/
@GetMapping("/rules/{id}")
public ApiResponse<RuleVO> getRule(@PathVariable Long id) {
log.info("获取分账规则详情, id: {}", id);
try {
RuleVO rule = profitShareIntegrationService.getRule(id);
return ApiResponse.success(rule);
} catch (Exception e) {
log.error("获取分账规则详情失败, id: {}", id, e);
return ApiResponse.fail("获取分账规则详情失败: " + e.getMessage());
}
}
/**
* 更新分账规则
*/
@PutMapping("/rules/{id}")
public ApiResponse<RuleVO> updateRule(@PathVariable Long id, @Valid @RequestBody CreateRuleRequest request) {
log.info("更新分账规则, id: {}", id);
try {
RuleVO rule = profitShareIntegrationService.updateRule(id, request);
return ApiResponse.success(rule);
} catch (Exception e) {
log.error("更新分账规则失败, id: {}", id, e);
return ApiResponse.fail("更新分账规则失败: " + e.getMessage());
}
}
/**
* 启用分账规则
*/
@PutMapping("/rules/{id}/enable")
public ApiResponse<String> enableRule(@PathVariable Long id) {
log.info("启用分账规则, id: {}", id);
try {
profitShareIntegrationService.enableRule(id);
return ApiResponse.success("规则已启用");
} catch (Exception e) {
log.error("启用分账规则失败, id: {}", id, e);
return ApiResponse.fail("启用分账规则失败: " + e.getMessage());
}
}
/**
* 禁用分账规则
*/
@PutMapping("/rules/{id}/disable")
public ApiResponse<String> disableRule(@PathVariable Long id) {
log.info("禁用分账规则, id: {}", id);
try {
profitShareIntegrationService.disableRule(id);
return ApiResponse.success("规则已禁用");
} catch (Exception e) {
log.error("禁用分账规则失败, id: {}", id, e);
return ApiResponse.fail("禁用分账规则失败: " + e.getMessage());
}
}
/**
* 删除分账规则
*/
@DeleteMapping("/rules/{id}")
public ApiResponse<String> deleteRule(@PathVariable Long id) {
log.info("删除分账规则, id: {}", id);
try {
profitShareIntegrationService.deleteRule(id);
return ApiResponse.success("规则已删除");
} catch (Exception e) {
log.error("删除分账规则失败, id: {}", id, e);
return ApiResponse.fail("删除分账规则失败: " + e.getMessage());
}
}
// ========== 分账记录查询 ==========
/**
* 查询景区分账记录
*/
@GetMapping("/records/scenic/{scenicId}")
public ApiResponse<PageResponse<RecordVO>> getRecordsByScenic(
@PathVariable Long scenicId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("查询景区分账记录, scenicId: {}, page: {}, pageSize: {}", scenicId, page, pageSize);
// 参数验证:限制pageSize最大值为100
if (pageSize > 100) {
pageSize = 100;
}
try {
PageResponse<RecordVO> response = profitShareIntegrationService.getRecordsByScenic(scenicId, page, pageSize);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("查询景区分账记录失败, scenicId: {}", scenicId, e);
return ApiResponse.fail("查询景区分账记录失败: " + e.getMessage());
}
}
/**
* 查询分账记录详情
*/
@GetMapping("/records/{id}")
public ApiResponse<RecordDetailVO> getRecordById(@PathVariable Long id) {
log.info("查询分账记录详情, id: {}", id);
try {
RecordDetailVO record = profitShareIntegrationService.getRecordById(id);
return ApiResponse.success(record);
} catch (Exception e) {
log.error("查询分账记录详情失败, id: {}", id, e);
return ApiResponse.fail("查询分账记录详情失败: " + e.getMessage());
}
}
/**
* 按订单ID查询分账记录
*/
@GetMapping("/records/order/{orderId}")
public ApiResponse<RecordDetailVO> getRecordByOrderId(@PathVariable String orderId) {
log.info("按订单ID查询分账记录, orderId: {}", orderId);
try {
RecordDetailVO record = profitShareIntegrationService.getRecordByOrderId(orderId);
return ApiResponse.success(record);
} catch (Exception e) {
log.error("按订单ID查询分账记录失败, orderId: {}", orderId, e);
return ApiResponse.fail("按订单ID查询分账记录失败: " + e.getMessage());
}
}
// ========== 手动分账与计算 ==========
/**
* 手动触发分账
*/
@PostMapping("/manual")
public ApiResponse<String> manualShare(@Valid @RequestBody ManualShareRequest request) {
log.info("手动触发分账, orderId: {}", request.getOrderId());
try {
profitShareIntegrationService.manualShare(request.getOrderId());
return ApiResponse.success("手动分账触发成功");
} catch (Exception e) {
log.error("手动触发分账失败, orderId: {}", request.getOrderId(), e);
return ApiResponse.fail("手动触发分账失败: " + e.getMessage());
}
}
/**
* 计算分账结果(不执行)
*/
@PostMapping("/calculate")
public ApiResponse<CalculateResultVO> calculateShare(@Valid @RequestBody CalculateShareRequest request) {
log.info("计算分账结果, scenicId: {}, totalAmount: {}", request.getScenicId(), request.getTotalAmount());
try {
CalculateResultVO result = profitShareIntegrationService.calculateShare(request);
return ApiResponse.success(result);
} catch (Exception e) {
log.error("计算分账结果失败", e);
return ApiResponse.fail("计算分账结果失败: " + e.getMessage());
}
}
// ========== 类型查询 ==========
/**
* 获取支持的类型列表
*/
@GetMapping("/types")
public ApiResponse<TypesVO> getSupportedTypes() {
log.info("获取支持的类型列表");
try {
TypesVO types = profitShareIntegrationService.getSupportedTypes();
return ApiResponse.success(types);
} catch (Exception e) {
log.error("获取支持的类型列表失败", e);
return ApiResponse.fail("获取支持的类型列表失败: " + e.getMessage());
}
}
}

View File

@@ -25,6 +25,7 @@ Currently implemented:
- **Device Integration** (`com.ycwl.basic.integration.device`): ZT-Device microservice integration
- **Render Worker Integration** (`com.ycwl.basic.integration.render`): ZT-Render-Worker microservice integration
- **Questionnaire Integration** (`com.ycwl.basic.integration.questionnaire`): ZT-Questionnaire microservice integration
- **Profit Share Integration** (`com.ycwl.basic.integration.profitshare`): ZT-ProfitShare microservice integration for revenue sharing
- **Message Integration** (`com.ycwl.basic.integration.message`): ZT-Message Kafka producer integration
### Integration Pattern
@@ -1714,4 +1715,418 @@ integration:
- Use appropriate page sizes for questionnaire lists
- Cache frequently accessed questionnaires
- Monitor response submission patterns
- Implement rate limiting for public questionnaires
- Implement rate limiting for public questionnaires
## Profit Share Integration (ZT-ProfitShare Microservice)
### Overview
The zt-profitshare microservice provides comprehensive revenue sharing management for scenic areas, supporting multiple payment systems (Alipay, WeChat, UnionPay) and various distribution rules. It offers both HTTP REST API for management operations and Kafka messaging for automatic profit sharing triggered by payment events.
### Key Components
#### Feign Client
- **ProfitShareClient**: Complete profit share operations (rules, records, manual sharing, calculations)
#### Services
- **ProfitShareIntegrationService**: High-level profit share operations (with automatic fallback for queries)
- **ProfitShareKafkaProducer**: Kafka message producer for profit share and refund events
#### Configuration
```yaml
integration:
profitshare:
enabled: true
serviceName: zt-profitshare
connectTimeout: 5000
readTimeout: 10000
retryEnabled: false
maxRetries: 3
fallback:
profitshare:
enabled: true
ttlDays: 7
kafka:
enabled: true
profit-share-topic: zt-profitshare
refund-topic: zt-refund
```
### Usage Examples
#### Rule Management Operations
```java
@Autowired
private ProfitShareIntegrationService profitShareService;
// Create profit share rule (direct operation, fails immediately on error)
CreateRuleRequest ruleRequest = new CreateRuleRequest();
ruleRequest.setScenicId(1001L);
ruleRequest.setRuleName("标准分账规则");
ruleRequest.setRuleType("percentage");
ruleRequest.setDescription("平台收取5%手续费");
// Add platform recipient
CreateRecipientRequest platform = new CreateRecipientRequest();
platform.setRecipientName("平台手续费");
platform.setRecipientType("platform");
platform.setAccountInfo("platform_001");
platform.setShareType("percentage");
platform.setShareValue(5.0);
platform.setPriority(1);
Map<String, Object> platformExt = new HashMap<>();
platformExt.put("payment_system", "alipay");
platformExt.put("sub_merchant_id", "platform_001");
platform.setExtConfig(platformExt);
// Add scenic recipient
CreateRecipientRequest scenic = new CreateRecipientRequest();
scenic.setRecipientName("景区收款账户");
scenic.setRecipientType("merchant");
scenic.setAccountInfo("merchant_001");
scenic.setShareType("percentage");
scenic.setShareValue(95.0);
scenic.setPriority(2);
Map<String, Object> scenicExt = new HashMap<>();
scenicExt.put("payment_system", "alipay");
scenicExt.put("sub_merchant_id", "scenic_001");
scenicExt.put("settle_period", "T+1");
scenic.setExtConfig(scenicExt);
ruleRequest.setRecipients(Arrays.asList(platform, scenic));
RuleVO createdRule = profitShareService.createRule(ruleRequest);
log.info("分账规则创建成功: {}", createdRule.getId());
// Get rule details (automatically falls back to cache on failure)
RuleVO rule = profitShareService.getRule(ruleId);
log.info("规则名称: {}, 状态: {}", rule.getRuleName(), rule.getStatus());
// List rules (automatically falls back to cache on failure)
PageResponse<RuleVO> rules = profitShareService.listRules(1001L, "active", "percentage", 1, 10);
log.info("查询到 {} 条规则", rules.getData().getTotal());
// Update rule (direct operation, fails immediately on error)
CreateRuleRequest updateRequest = new CreateRuleRequest();
updateRequest.setRuleName("更新后的分账规则");
RuleVO updated = profitShareService.updateRule(ruleId, updateRequest);
// Enable/disable rule (direct operations, fail immediately on error)
profitShareService.enableRule(ruleId);
profitShareService.disableRule(ruleId);
// Delete rule (direct operation, fails immediately on error)
profitShareService.deleteRule(ruleId);
```
#### Record Query Operations (with Automatic Fallback)
```java
// Get scenic profit share records (automatically falls back to cache on failure)
PageResponse<RecordVO> records = profitShareService.getRecordsByScenic(1001L, 1, 10);
log.info("景区分账记录: {} 条", records.getData().getTotal());
records.getData().getList().forEach(record -> {
log.info("订单: {}, 金额: {}, 状态: {}",
record.getOrderId(), record.getTotalAmount(), record.getStatus());
});
// Get record detail by ID (automatically falls back to cache on failure)
RecordDetailVO detail = profitShareService.getRecordById(recordId);
log.info("分账记录详情:");
detail.getDetails().forEach(shareDetail -> {
log.info(" 接收人: {}, 金额: {}, 状态: {}",
shareDetail.getRecipientName(), shareDetail.getShareAmount(), shareDetail.getStatus());
});
// Get record by order ID (automatically falls back to cache on failure)
RecordDetailVO recordByOrder = profitShareService.getRecordByOrderId("ORDER_123456");
```
#### Kafka Message Production
```java
@Autowired
private ProfitShareKafkaProducer profitShareProducer;
// Send profit share message after payment success
@Transactional
public void handleOrderPaymentSuccess(Order order) {
// 1. Update order status
order.setStatus("PAID");
order.setPaymentTime(new Date());
orderRepository.save(order);
// 2. Build profit share message
OrderMessage message = OrderMessage.of(
order.getOrderId(),
order.getScenicId(),
order.getTotalAmount().doubleValue(),
order.getPaymentChannel(), // "alipay", "wechat", "union"
order.getPaymentOrderId()
);
// 3. Send to Kafka (async profit sharing)
profitShareProducer.sendProfitShareMessage(message);
log.info("订单支付成功,已发送分账消息: orderId={}, amount={}",
order.getOrderId(), order.getTotalAmount());
}
// Send refund message after refund success
@Transactional
public void handleOrderRefundSuccess(Order order, BigDecimal refundAmount) {
// 1. Update order status
order.setStatus("REFUNDED");
orderRepository.save(order);
// 2. Build refund message
RefundMessage message = RefundMessage.of(
order.getOrderId(),
order.getScenicId(),
refundAmount.doubleValue(),
order.getPaymentChannel(),
order.getRefundOrderId()
);
// 3. Send refund message
profitShareProducer.sendRefundMessage(message);
log.info("订单退款成功,已发送退款消息: orderId={}, amount={}",
order.getOrderId(), refundAmount);
}
```
#### Manual Profit Sharing
```java
// Manual trigger profit sharing (direct operation, fails immediately on error)
// Used for compensation scenarios or delayed profit sharing
profitShareService.manualShare("ORDER_123456");
log.info("手动分账触发成功");
```
#### Calculate Profit Share
```java
// Calculate profit share without execution (automatically falls back to cache on failure)
CalculateShareRequest calcRequest = new CalculateShareRequest();
calcRequest.setScenicId(1001L);
calcRequest.setTotalAmount(1000.0);
calcRequest.setRuleType("percentage");
calcRequest.setRecipients(Arrays.asList(platform, scenic));
CalculateResultVO result = profitShareService.calculateShare(calcRequest);
log.info("总金额: {}, 分账明细:", result.getTotalAmount());
result.getDetails().forEach(detail -> {
log.info(" {}: {} 元", detail.getRecipientName(), detail.getShareAmount());
});
// Get supported types (automatically falls back to cache on failure)
TypesVO types = profitShareService.getSupportedTypes();
log.info("支持的规则类型: {}", types.getRuleTypes());
log.info("支持的接收人类型: {}", types.getRecipientTypes());
```
#### Fallback Cache Management
```java
@Autowired
private IntegrationFallbackService fallbackService;
// Check fallback cache status
boolean hasRuleCache = fallbackService.hasFallbackCache("zt-profitshare", "rule:1001");
boolean hasRecordsCache = fallbackService.hasFallbackCache("zt-profitshare", "records:scenic:1001:1:10");
// Get cache statistics
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats("zt-profitshare");
log.info("Profit share fallback cache: {} items, TTL: {} days",
stats.getTotalCacheCount(), stats.getFallbackTtlDays());
// Clear specific cache
fallbackService.clearFallbackCache("zt-profitshare", "rule:1001");
// Clear all profit share caches
fallbackService.clearAllFallbackCache("zt-profitshare");
```
### Rule Types and Configuration
#### Rule Types
- **percentage**: Percentage-based distribution (e.g., platform 5%, merchant 95%)
- **fixed_amount**: Fixed amount distribution
- **scaled_amount**: Tiered amount distribution based on order amount
#### Recipient Types
- **platform**: Platform service fee recipient
- **merchant**: Merchant/scenic area recipient
- **agent**: Agent/distributor recipient
#### Share Types
- **percentage**: Share as percentage of total amount
- **fixed_amount**: Share as fixed amount
#### Payment Systems
- **alipay**: Alipay payment system
- **wechat**: WeChat Pay payment system
- **union**: UnionPay payment system
### Extended Configuration Examples
#### Alipay Configuration
```json
{
"payment_system": "alipay",
"sub_merchant_id": "2088xxx",
"settle_period": "T+1",
"account_type": "ALIPAY_LOGON_ID"
}
```
#### WeChat Configuration
```json
{
"payment_system": "wechat",
"sub_mch_id": "1234567890",
"settle_period": "T+0",
"account_type": "MERCHANT_ID"
}
```
#### Tiered Sharing Configuration
```json
{
"scales": [
{
"min_amount": 0,
"max_amount": 1000,
"share_value": 3.0,
"share_type": "percentage"
},
{
"min_amount": 1000,
"max_amount": 5000,
"share_value": 5.0,
"share_type": "percentage"
},
{
"min_amount": 5000,
"max_amount": 0,
"share_value": 8.0,
"share_type": "percentage"
}
]
}
```
### Record Status
- **pending**: Profit share request pending
- **processing**: Profit share in progress
- **success**: Profit share completed successfully
- **failed**: Profit share failed (requires manual intervention)
### Cache Key Design
- `rule:{id}` - Individual rule cache
- `rules:list:{scenicId}:{status}:{ruleType}:{page}:{size}` - Rule list cache
- `record:{id}` - Individual record cache
- `record:order:{orderId}` - Record by order ID cache
- `records:scenic:{scenicId}:{page}:{size}` - Scenic records cache
- `calculate:{scenicId}:{amount}` - Calculation cache
- `types` - Supported types cache
### Best Practices
#### When to Use HTTP API vs Kafka
- **HTTP API**:
- Rule management (create, update, enable/disable, delete)
- Query operations (records, statistics)
- Manual compensation scenarios
- Profit share calculation (dry run)
- **Kafka Messages** (Recommended):
- Automatic profit sharing after order payment
- Automatic reversal after order refund
- Asynchronous processing with retry capability
- Decoupled from main order flow
#### Idempotency Handling
```java
// Check before sending Kafka message
if (profitShareRecordRepository.existsByOrderId(orderId)) {
log.warn("订单已发起分账,跳过: orderId={}", orderId);
return;
}
// Record profit share request
ProfitShareRequest request = new ProfitShareRequest();
request.setOrderId(orderId);
request.setStatus("PENDING");
request.setCreatedAt(new Date());
profitShareRecordRepository.save(request);
// Send Kafka message
profitShareProducer.sendProfitShareMessage(message);
```
#### Monitoring and Alerts
```java
// Monitor Kafka message send success rate
metricRegistry.counter("profit_share.kafka.send.success").inc();
metricRegistry.counter("profit_share.kafka.send.failure").inc();
// Monitor profit share record status
metricRegistry.gauge("profit_share.records.pending", () ->
profitShareRecordRepository.countByStatus("pending"));
metricRegistry.gauge("profit_share.records.failed", () ->
profitShareRecordRepository.countByStatus("failed"));
// Alert on failures
if (!response.getSuccess()) {
alertService.send("分账服务异常", response.getMessage());
}
```
### Common Issues and Solutions
#### Q1: Kafka message sent but no profit share record created?
**A**: Troubleshooting steps:
1. Check Kafka broker connectivity
2. Verify topic `zt-profitshare` exists
3. Check profit share service logs for consumption errors
4. Query record by order ID to verify processing status
#### Q2: Profit share calculation incorrect?
**A**: Verify:
- Amount unit is in Yuan (), not Fen ()
- Percentage shares sum up to 100% or less
- Check `min_amount` and `max_amount` limits
- Review recipient priority ordering
#### Q3: How to handle profit share failures?
**A**: Profit share service automatically retries (max 3 times). If still fails:
1. Query record detail for error message
2. Fix data or rule issues
3. Call manual share API to retry
4. Set up alerts for failed records
#### Q4: Can I modify rule after orders are processed?
**A**: Yes, but:
- New orders use the updated rule
- Existing profit share records are not affected
- Consider creating new rule instead of modifying active one
- Disable old rule and enable new one for clean transition
### Testing Profit Share Integration
```bash
# Run profit share integration tests
mvn test -Dtest=ProfitShareIntegrationServiceTest
# Run all integration tests
mvn test -Dtest="com.ycwl.basic.integration.*Test"
```

View File

@@ -12,6 +12,8 @@ public class KafkaIntegrationProperties {
private boolean enabled = false;
private String bootstrapServers = "100.64.0.12:39092";
private String ztMessageTopic = "zt-message"; // topic for zt-message microservice
private String profitShareTopic = "zt-profitshare"; // topic for profit share messages
private String refundTopic = "zt-profitshare-refund"; // topic for refund messages
private Consumer consumer = new Consumer();
private Producer producer = new Producer();

View File

@@ -0,0 +1,109 @@
package com.ycwl.basic.integration.profitshare.client;
import com.ycwl.basic.integration.common.response.CommonResponse;
import com.ycwl.basic.integration.common.response.PageResponse;
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 org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* 分账服务Feign客户端
*
* @author Claude Code
* @date 2025-01-11
*/
@FeignClient(name = "zt-profitshare", contextId = "profit-share-v2", path = "/api/profit-share/v2")
public interface ProfitShareClient {
/**
* 创建分账规则
*/
@PostMapping("/rules")
CommonResponse<RuleVO> createRule(@RequestBody CreateRuleRequest request);
/**
* 查询分账规则列表
*/
@GetMapping("/rules")
CommonResponse<PageResponse<RuleVO>> listRules(@RequestParam(value = "scenic_id", required = false) Long scenicId,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "rule_type", required = false) String ruleType,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "page_size", defaultValue = "10") Integer pageSize);
/**
* 获取分账规则详情
*/
@GetMapping("/rules/{id}")
CommonResponse<RuleVO> getRule(@PathVariable("id") Long ruleId);
/**
* 更新分账规则
*/
@PutMapping("/rules/{id}")
CommonResponse<RuleVO> updateRule(@PathVariable("id") Long ruleId,
@RequestBody CreateRuleRequest request);
/**
* 删除分账规则
*/
@DeleteMapping("/rules/{id}")
CommonResponse<Void> deleteRule(@PathVariable("id") Long ruleId);
/**
* 启用规则
*/
@PutMapping("/rules/{id}/enable")
CommonResponse<Void> enableRule(@PathVariable("id") Long ruleId);
/**
* 禁用规则
*/
@PutMapping("/rules/{id}/disable")
CommonResponse<Void> disableRule(@PathVariable("id") Long ruleId);
/**
* 查询景区分账记录
*/
@GetMapping("/records/scenic/{scenic_id}")
CommonResponse<PageResponse<RecordVO>> getRecordsByScenic(@PathVariable("scenic_id") Long scenicId,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "page_size", defaultValue = "10") Integer pageSize);
/**
* 查询分账记录详情
*/
@GetMapping("/records/{id}")
CommonResponse<RecordDetailVO> getRecordById(@PathVariable("id") Long recordId);
/**
* 按订单ID查询分账记录
*/
@GetMapping("/records/order/{order_id}")
CommonResponse<RecordDetailVO> getRecordByOrderId(@PathVariable("order_id") String orderId);
/**
* 手动触发分账
*/
@PostMapping("/manual")
CommonResponse<Void> manualShare(@RequestBody ManualShareRequest request);
/**
* 计算分账金额(不执行)
*/
@PostMapping("/calculate")
CommonResponse<CalculateResultVO> calculateShare(@RequestBody CalculateShareRequest request);
/**
* 获取支持的类型
*/
@GetMapping("/types")
CommonResponse<TypesVO> getSupportedTypes();
}

View File

@@ -0,0 +1,21 @@
package com.ycwl.basic.integration.profitshare.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 分账服务集成配置
*
* @author Claude Code
* @date 2025-01-11
*/
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "integration.profitshare")
public class ProfitShareIntegrationConfig {
public ProfitShareIntegrationConfig() {
log.info("ZT-ProfitShare集成配置初始化完成");
}
}

View File

@@ -0,0 +1,77 @@
package com.ycwl.basic.integration.profitshare.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* 计算分账结果VO
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CalculateResultVO {
/**
* 总金额
*/
@JsonProperty("total_amount")
private Double totalAmount;
/**
* 分账明细列表
*/
@JsonProperty("details")
private List<CalculateDetailVO> details;
/**
* 分账明细VO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class CalculateDetailVO {
/**
* 接收人名称
*/
@JsonProperty("recipient_name")
private String recipientName;
/**
* 接收人类型
*/
@JsonProperty("recipient_type")
private String recipientType;
/**
* 分账金额
*/
@JsonProperty("share_amount")
private Double shareAmount;
/**
* 分账类型
*/
@JsonProperty("share_type")
private String shareType;
/**
* 分账值
*/
@JsonProperty("share_value")
private Double shareValue;
}
}

View File

@@ -0,0 +1,49 @@
package com.ycwl.basic.integration.profitshare.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ycwl.basic.integration.profitshare.dto.rule.CreateRecipientRequest;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 计算分账请求
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CalculateShareRequest {
/**
* 景区ID
*/
@JsonProperty("scenic_id")
private Long scenicId;
/**
* 总金额
*/
@JsonProperty("total_amount")
private Double totalAmount;
/**
* 规则类型
*/
@JsonProperty("rule_type")
private String ruleType;
/**
* 分账接收人列表
*/
@JsonProperty("recipients")
private List<CreateRecipientRequest> recipients;
}

View File

@@ -0,0 +1,28 @@
package com.ycwl.basic.integration.profitshare.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 手动分账请求
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ManualShareRequest {
/**
* 订单ID
*/
@JsonProperty("order_id")
private String orderId;
}

View File

@@ -0,0 +1,48 @@
package com.ycwl.basic.integration.profitshare.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 支持的类型VO
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TypesVO {
/**
* 规则类型列表
*/
@JsonProperty("rule_types")
private List<String> ruleTypes;
/**
* 接收人类型列表
*/
@JsonProperty("recipient_types")
private List<String> recipientTypes;
/**
* 分账类型列表
*/
@JsonProperty("share_types")
private List<String> shareTypes;
/**
* 状态列表
*/
@JsonProperty("statuses")
private List<String> statuses;
}

View File

@@ -0,0 +1,72 @@
package com.ycwl.basic.integration.profitshare.dto.message;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 订单分账消息(发送到 zt-profitshare topic)
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OrderMessage {
/**
* 订单ID
*/
@JsonProperty("order_id")
private String orderId;
/**
* 景区ID
*/
@JsonProperty("scenic_id")
private Long scenicId;
/**
* 总金额(单位:元)
*/
@JsonProperty("total_amount")
private Double totalAmount;
/**
* 支付系统(alipay, wechat, union)
*/
@JsonProperty("payment_system")
private String paymentSystem;
/**
* 支付订单ID
*/
@JsonProperty("payment_order_id")
private String paymentOrderId;
/**
* Unix 时间戳(秒)
*/
@JsonProperty("timestamp")
private Long timestamp;
/**
* 快速创建订单消息
*/
public static OrderMessage of(String orderId, Long scenicId, Double totalAmount, String paymentSystem, String paymentOrderId) {
return OrderMessage.builder()
.orderId(orderId)
.scenicId(scenicId)
.totalAmount(totalAmount)
.paymentSystem(paymentSystem)
.paymentOrderId(paymentOrderId)
.timestamp(System.currentTimeMillis() / 1000)
.build();
}
}

View File

@@ -0,0 +1,72 @@
package com.ycwl.basic.integration.profitshare.dto.message;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 退款消息(发送到 zt-profitshare-refund topic)
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RefundMessage {
/**
* 退款订单ID
*/
@JsonProperty("refund_order_id")
private String refundOrderId;
/**
* 原订单ID
*/
@JsonProperty("original_order_id")
private String originalOrderId;
/**
* 退款金额(单位:元)
*/
@JsonProperty("refund_amount")
private Double refundAmount;
/**
* 退款类型(full: 全额退款, partial: 部分退款)
*/
@JsonProperty("refund_type")
private String refundType;
/**
* 支付系统(alipay, wechat, union)
*/
@JsonProperty("payment_system")
private String paymentSystem;
/**
* Unix 时间戳(秒)
*/
@JsonProperty("timestamp")
private Long timestamp;
/**
* 快速创建退款消息
*/
public static RefundMessage of(String refundOrderId, String originalOrderId, Double refundAmount, String refundType, String paymentSystem) {
return RefundMessage.builder()
.refundOrderId(refundOrderId)
.originalOrderId(originalOrderId)
.refundAmount(refundAmount)
.refundType(refundType)
.paymentSystem(paymentSystem)
.timestamp(System.currentTimeMillis() / 1000)
.build();
}
}

View File

@@ -0,0 +1,96 @@
package com.ycwl.basic.integration.profitshare.dto.record;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 分账记录详情VO
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RecordDetailVO {
/**
* 记录ID
*/
@JsonProperty("id")
private Long id;
/**
* 订单ID
*/
@JsonProperty("order_id")
private String orderId;
/**
* 景区ID
*/
@JsonProperty("scenic_id")
private Long scenicId;
/**
* 规则ID
*/
@JsonProperty("rule_id")
private Long ruleId;
/**
* 总金额
*/
@JsonProperty("total_amount")
private Double totalAmount;
/**
* 支付系统
*/
@JsonProperty("payment_system")
private String paymentSystem;
/**
* 支付订单ID
*/
@JsonProperty("payment_order_id")
private String paymentOrderId;
/**
* 状态
*/
@JsonProperty("status")
private String status;
/**
* 错误信息
*/
@JsonProperty("error_message")
private String errorMessage;
/**
* 分账明细列表
*/
@JsonProperty("details")
private List<ShareDetailVO> details;
/**
* 创建时间
*/
@JsonProperty("created_at")
private String createdAt;
/**
* 更新时间
*/
@JsonProperty("updated_at")
private String updatedAt;
}

View File

@@ -0,0 +1,88 @@
package com.ycwl.basic.integration.profitshare.dto.record;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 分账记录VO
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RecordVO {
/**
* 记录ID
*/
@JsonProperty("id")
private Long id;
/**
* 订单ID
*/
@JsonProperty("order_id")
private String orderId;
/**
* 景区ID
*/
@JsonProperty("scenic_id")
private Long scenicId;
/**
* 规则ID
*/
@JsonProperty("rule_id")
private Long ruleId;
/**
* 总金额
*/
@JsonProperty("total_amount")
private Double totalAmount;
/**
* 支付系统
*/
@JsonProperty("payment_system")
private String paymentSystem;
/**
* 支付订单ID
*/
@JsonProperty("payment_order_id")
private String paymentOrderId;
/**
* 状态(pending, processing, success, failed)
*/
@JsonProperty("status")
private String status;
/**
* 错误信息
*/
@JsonProperty("error_message")
private String errorMessage;
/**
* 创建时间
*/
@JsonProperty("created_at")
private String createdAt;
/**
* 更新时间
*/
@JsonProperty("updated_at")
private String updatedAt;
}

View File

@@ -0,0 +1,64 @@
package com.ycwl.basic.integration.profitshare.dto.record;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 分账明细VO
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ShareDetailVO {
/**
* 明细ID
*/
@JsonProperty("id")
private Long id;
/**
* 接收人名称
*/
@JsonProperty("recipient_name")
private String recipientName;
/**
* 接收人类型
*/
@JsonProperty("recipient_type")
private String recipientType;
/**
* 账户信息
*/
@JsonProperty("account_info")
private String accountInfo;
/**
* 分账金额
*/
@JsonProperty("share_amount")
private Double shareAmount;
/**
* 状态
*/
@JsonProperty("status")
private String status;
/**
* 错误信息
*/
@JsonProperty("error_message")
private String errorMessage;
}

View File

@@ -0,0 +1,78 @@
package com.ycwl.basic.integration.profitshare.dto.rule;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 分账接收人请求
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CreateRecipientRequest {
/**
* 接收人名称
*/
@JsonProperty("recipient_name")
private String recipientName;
/**
* 接收人类型(platform, merchant, agent)
*/
@JsonProperty("recipient_type")
private String recipientType;
/**
* 账户信息
*/
@JsonProperty("account_info")
private String accountInfo;
/**
* 分账类型(percentage, fixed_amount)
*/
@JsonProperty("share_type")
private String shareType;
/**
* 分账值(百分比或固定金额)
*/
@JsonProperty("share_value")
private Double shareValue;
/**
* 最小分账金额
*/
@JsonProperty("min_amount")
private Double minAmount;
/**
* 最大分账金额
*/
@JsonProperty("max_amount")
private Double maxAmount;
/**
* 优先级
*/
@JsonProperty("priority")
private Integer priority;
/**
* 扩展配置
*/
@JsonProperty("ext_config")
private Map<String, Object> extConfig;
}

View File

@@ -0,0 +1,54 @@
package com.ycwl.basic.integration.profitshare.dto.rule;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 创建分账规则请求
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CreateRuleRequest {
/**
* 景区ID
*/
@JsonProperty("scenic_id")
private Long scenicId;
/**
* 规则名称
*/
@JsonProperty("rule_name")
private String ruleName;
/**
* 规则类型(percentage, fixed_amount, scaled_amount)
*/
@JsonProperty("rule_type")
private String ruleType;
/**
* 规则描述
*/
@JsonProperty("description")
private String description;
/**
* 分账接收人列表
*/
@JsonProperty("recipients")
private List<CreateRecipientRequest> recipients;
}

View File

@@ -0,0 +1,90 @@
package com.ycwl.basic.integration.profitshare.dto.rule;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 分账接收人VO
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RecipientVO {
/**
* 接收人ID
*/
@JsonProperty("id")
private Long id;
/**
* 接收人名称
*/
@JsonProperty("recipient_name")
private String recipientName;
/**
* 接收人类型
*/
@JsonProperty("recipient_type")
private String recipientType;
/**
* 账户信息
*/
@JsonProperty("account_info")
private String accountInfo;
/**
* 分账类型
*/
@JsonProperty("share_type")
private String shareType;
/**
* 分账值
*/
@JsonProperty("share_value")
private Double shareValue;
/**
* 最小分账金额
*/
@JsonProperty("min_amount")
private Double minAmount;
/**
* 最大分账金额
*/
@JsonProperty("max_amount")
private Double maxAmount;
/**
* 优先级
*/
@JsonProperty("priority")
private Integer priority;
/**
* 扩展配置
*/
@JsonProperty("ext_config")
private Map<String, Object> extConfig;
/**
* 状态
*/
@JsonProperty("status")
private String status;
}

View File

@@ -0,0 +1,78 @@
package com.ycwl.basic.integration.profitshare.dto.rule;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 分账规则VO
*
* @author Claude Code
* @date 2025-01-11
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RuleVO {
/**
* 规则ID
*/
@JsonProperty("id")
private Long id;
/**
* 景区ID
*/
@JsonProperty("scenic_id")
private Long scenicId;
/**
* 规则名称
*/
@JsonProperty("rule_name")
private String ruleName;
/**
* 规则类型
*/
@JsonProperty("rule_type")
private String ruleType;
/**
* 规则描述
*/
@JsonProperty("description")
private String description;
/**
* 状态(active, inactive)
*/
@JsonProperty("status")
private Integer status;
/**
* 分账接收人列表
*/
@JsonProperty("recipients")
private List<RecipientVO> recipients;
/**
* 创建时间
*/
@JsonProperty("created_at")
private String createdAt;
/**
* 更新时间
*/
@JsonProperty("updated_at")
private String updatedAt;
}

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-profitshare-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.getOriginalOrderId();
String payload = toJson(message);
log.info("[REFUND] producing to topic={}, key={}, refundOrderId={}, originalOrderId={}, amount={}, type={}",
topic, key, message.getRefundOrderId(), message.getOriginalOrderId(), message.getRefundAmount(), message.getRefundType());
kafkaTemplate.send(topic, key, payload).whenComplete((metadata, ex) -> {
if (ex != null) {
log.error("[REFUND] produce failed: refundOrderId={}, error={}", message.getRefundOrderId(), ex.getMessage(), ex);
} else if (metadata != null) {
log.info("[REFUND] produced: refundOrderId={}, partition={}, offset={}",
message.getRefundOrderId(), 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.getRefundOrderId())) {
throw new IllegalArgumentException("refundOrderId is required");
}
if (StringUtils.isBlank(msg.getOriginalOrderId())) {
throw new IllegalArgumentException("originalOrderId is required");
}
if (msg.getRefundAmount() == null || msg.getRefundAmount() <= 0) {
throw new IllegalArgumentException("refundAmount is required and must be positive");
}
if (StringUtils.isBlank(msg.getRefundType())) {
throw new IllegalArgumentException("refundType is required");
}
if (StringUtils.isBlank(msg.getPaymentSystem())) {
throw new IllegalArgumentException("paymentSystem is required");
}
}
private String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("failed to serialize message", e);
}
}
}