diff --git a/src/main/java/com/ycwl/basic/controller/pc/ProfitShareV2Controller.java b/src/main/java/com/ycwl/basic/controller/pc/ProfitShareV2Controller.java new file mode 100644 index 00000000..d7a7b49f --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/pc/ProfitShareV2Controller.java @@ -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 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> 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 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 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 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 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 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 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> 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 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 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 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 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 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 getSupportedTypes() { + log.info("获取支持的类型列表"); + try { + TypesVO types = profitShareIntegrationService.getSupportedTypes(); + return ApiResponse.success(types); + } catch (Exception e) { + log.error("获取支持的类型列表失败", e); + return ApiResponse.fail("获取支持的类型列表失败: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/ycwl/basic/integration/CLAUDE.md b/src/main/java/com/ycwl/basic/integration/CLAUDE.md index bc1b33d2..50d24bab 100644 --- a/src/main/java/com/ycwl/basic/integration/CLAUDE.md +++ b/src/main/java/com/ycwl/basic/integration/CLAUDE.md @@ -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 \ No newline at end of file +- 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 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 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 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 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" +``` \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/kafka/config/KafkaIntegrationProperties.java b/src/main/java/com/ycwl/basic/integration/kafka/config/KafkaIntegrationProperties.java index 6457e0f3..c180521e 100644 --- a/src/main/java/com/ycwl/basic/integration/kafka/config/KafkaIntegrationProperties.java +++ b/src/main/java/com/ycwl/basic/integration/kafka/config/KafkaIntegrationProperties.java @@ -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(); diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/client/ProfitShareClient.java b/src/main/java/com/ycwl/basic/integration/profitshare/client/ProfitShareClient.java new file mode 100644 index 00000000..6e5859f3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/client/ProfitShareClient.java @@ -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 createRule(@RequestBody CreateRuleRequest request); + + /** + * 查询分账规则列表 + */ + @GetMapping("/rules") + CommonResponse> 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 getRule(@PathVariable("id") Long ruleId); + + /** + * 更新分账规则 + */ + @PutMapping("/rules/{id}") + CommonResponse updateRule(@PathVariable("id") Long ruleId, + @RequestBody CreateRuleRequest request); + + /** + * 删除分账规则 + */ + @DeleteMapping("/rules/{id}") + CommonResponse deleteRule(@PathVariable("id") Long ruleId); + + /** + * 启用规则 + */ + @PutMapping("/rules/{id}/enable") + CommonResponse enableRule(@PathVariable("id") Long ruleId); + + /** + * 禁用规则 + */ + @PutMapping("/rules/{id}/disable") + CommonResponse disableRule(@PathVariable("id") Long ruleId); + + /** + * 查询景区分账记录 + */ + @GetMapping("/records/scenic/{scenic_id}") + CommonResponse> 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 getRecordById(@PathVariable("id") Long recordId); + + /** + * 按订单ID查询分账记录 + */ + @GetMapping("/records/order/{order_id}") + CommonResponse getRecordByOrderId(@PathVariable("order_id") String orderId); + + /** + * 手动触发分账 + */ + @PostMapping("/manual") + CommonResponse manualShare(@RequestBody ManualShareRequest request); + + /** + * 计算分账金额(不执行) + */ + @PostMapping("/calculate") + CommonResponse calculateShare(@RequestBody CalculateShareRequest request); + + /** + * 获取支持的类型 + */ + @GetMapping("/types") + CommonResponse getSupportedTypes(); +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/config/ProfitShareIntegrationConfig.java b/src/main/java/com/ycwl/basic/integration/profitshare/config/ProfitShareIntegrationConfig.java new file mode 100644 index 00000000..74fbb1ca --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/config/ProfitShareIntegrationConfig.java @@ -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集成配置初始化完成"); + } +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/CalculateResultVO.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/CalculateResultVO.java new file mode 100644 index 00000000..f238a6e3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/CalculateResultVO.java @@ -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 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; + } +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/CalculateShareRequest.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/CalculateShareRequest.java new file mode 100644 index 00000000..80982e35 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/CalculateShareRequest.java @@ -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 recipients; +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/ManualShareRequest.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/ManualShareRequest.java new file mode 100644 index 00000000..2a1aba4f --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/ManualShareRequest.java @@ -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; +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/TypesVO.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/TypesVO.java new file mode 100644 index 00000000..b83efb55 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/TypesVO.java @@ -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 ruleTypes; + + /** + * 接收人类型列表 + */ + @JsonProperty("recipient_types") + private List recipientTypes; + + /** + * 分账类型列表 + */ + @JsonProperty("share_types") + private List shareTypes; + + /** + * 状态列表 + */ + @JsonProperty("statuses") + private List statuses; +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/message/OrderMessage.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/message/OrderMessage.java new file mode 100644 index 00000000..1164f92c --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/message/OrderMessage.java @@ -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(); + } +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/message/RefundMessage.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/message/RefundMessage.java new file mode 100644 index 00000000..e1862fd6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/message/RefundMessage.java @@ -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(); + } +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/record/RecordDetailVO.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/record/RecordDetailVO.java new file mode 100644 index 00000000..1d7ea8f0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/record/RecordDetailVO.java @@ -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 details; + + /** + * 创建时间 + */ + @JsonProperty("created_at") + private String createdAt; + + /** + * 更新时间 + */ + @JsonProperty("updated_at") + private String updatedAt; +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/record/RecordVO.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/record/RecordVO.java new file mode 100644 index 00000000..abe2b513 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/record/RecordVO.java @@ -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; +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/record/ShareDetailVO.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/record/ShareDetailVO.java new file mode 100644 index 00000000..0a76c0df --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/record/ShareDetailVO.java @@ -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; +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/CreateRecipientRequest.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/CreateRecipientRequest.java new file mode 100644 index 00000000..95248a91 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/CreateRecipientRequest.java @@ -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 extConfig; +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/CreateRuleRequest.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/CreateRuleRequest.java new file mode 100644 index 00000000..66e33c3d --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/CreateRuleRequest.java @@ -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 recipients; +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/RecipientVO.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/RecipientVO.java new file mode 100644 index 00000000..4efd851a --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/RecipientVO.java @@ -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 extConfig; + + /** + * 状态 + */ + @JsonProperty("status") + private String status; +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/RuleVO.java b/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/RuleVO.java new file mode 100644 index 00000000..e79cc216 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/dto/rule/RuleVO.java @@ -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 recipients; + + /** + * 创建时间 + */ + @JsonProperty("created_at") + private String createdAt; + + /** + * 更新时间 + */ + @JsonProperty("updated_at") + private String updatedAt; +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/service/ProfitShareIntegrationService.java b/src/main/java/com/ycwl/basic/integration/profitshare/service/ProfitShareIntegrationService.java new file mode 100644 index 00000000..40a2347d --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/service/ProfitShareIntegrationService.java @@ -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 response = profitShareClient.createRule(request); + return handleResponse(response, "创建分账规则失败"); + } + + /** + * 查询分账规则列表(带fallback) + */ + public PageResponse 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> 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 response = profitShareClient.getRule(ruleId); + return handleResponse(response, "获取分账规则详情失败"); + }, + RuleVO.class + ); + } + + /** + * 更新分账规则(直接操作,无fallback) + */ + public RuleVO updateRule(Long ruleId, CreateRuleRequest request) { + log.debug("更新分账规则, ruleId: {}", ruleId); + CommonResponse response = profitShareClient.updateRule(ruleId, request); + return handleResponse(response, "更新分账规则失败"); + } + + /** + * 删除分账规则(直接操作,无fallback) + */ + public void deleteRule(Long ruleId) { + log.debug("删除分账规则, ruleId: {}", ruleId); + CommonResponse response = profitShareClient.deleteRule(ruleId); + handleResponse(response, "删除分账规则失败"); + } + + /** + * 启用规则(直接操作,无fallback) + */ + public void enableRule(Long ruleId) { + log.debug("启用分账规则, ruleId: {}", ruleId); + CommonResponse response = profitShareClient.enableRule(ruleId); + handleResponse(response, "启用分账规则失败"); + } + + /** + * 禁用规则(直接操作,无fallback) + */ + public void disableRule(Long ruleId) { + log.debug("禁用分账规则, ruleId: {}", ruleId); + CommonResponse response = profitShareClient.disableRule(ruleId); + handleResponse(response, "禁用分账规则失败"); + } + + // ==================== 分账记录查询 ==================== + + /** + * 查询景区分账记录(带fallback) + */ + public PageResponse 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> 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 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 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 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 response = profitShareClient.calculateShare(request); + return handleResponse(response, "计算分账金额失败"); + }, + CalculateResultVO.class + ); + } + + /** + * 获取支持的类型(带fallback) + */ + public TypesVO getSupportedTypes() { + log.debug("获取支持的类型"); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "types", + () -> { + CommonResponse response = profitShareClient.getSupportedTypes(); + return handleResponse(response, "获取支持的类型失败"); + }, + TypesVO.class + ); + } + + // ==================== 私有方法 ==================== + + private T handleResponse(CommonResponse 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(); + } +} diff --git a/src/main/java/com/ycwl/basic/integration/profitshare/service/ProfitShareKafkaProducer.java b/src/main/java/com/ycwl/basic/integration/profitshare/service/ProfitShareKafkaProducer.java new file mode 100644 index 00000000..c1a509e0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/profitshare/service/ProfitShareKafkaProducer.java @@ -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 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); + } + } +}