# 支付服务开发文档 ## 一、Entity 实体类 ### RechargeOrder.java ```java package com.openclaw.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @TableName("recharge_orders") public class RechargeOrder { @TableId(type = IdType.AUTO) private Long id; private String rechargeNo; private Long userId; private BigDecimal amount; private Integer bonusPoints; private Integer totalPoints; private String paymentMethod; // wechat / alipay private String status; // pending/paid/failed/cancelled private String transactionId; private String notifyData; // 回调原始数据 private LocalDateTime createdAt; private LocalDateTime updatedAt; private LocalDateTime paidAt; } ``` ### PaymentRecord.java ```java package com.openclaw.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @TableName("payment_records") public class PaymentRecord { @TableId(type = IdType.AUTO) private Long id; private Long userId; private String bizType; // order / recharge private Long bizId; private String bizNo; private BigDecimal amount; private String paymentMethod; private String transactionId; private String status; // pending/success/failed private LocalDateTime createdAt; private LocalDateTime updatedAt; } ``` ## 二、DTO / VO ### RechargeDTO.java ```java package com.openclaw.dto; import jakarta.validation.constraints.*; import lombok.Data; import java.math.BigDecimal; @Data public class RechargeDTO { @NotNull(message = "充值金额不能为空") @DecimalMin(value = "1.00", message = "最低充值金额1元") private BigDecimal amount; @NotBlank(message = "支付方式不能为空") private String paymentMethod; // wechat / alipay } ``` ### RechargeVO.java ```java package com.openclaw.vo; import lombok.Data; import java.math.BigDecimal; @Data public class RechargeVO { private Long rechargeId; private String rechargeNo; private BigDecimal amount; private Integer bonusPoints; private Integer totalPoints; // 支付参数(前端拉起支付用) private String payParams; // JSON字符串,微信/支付宝支付参数 } ``` ### PaymentRecordVO.java ```java package com.openclaw.vo; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data public class PaymentRecordVO { private Long id; private String bizType; private String bizNo; private BigDecimal amount; private String paymentMethod; private String status; private LocalDateTime createdAt; } ``` ## 三、充值赠送规则配置 ```java package com.openclaw.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.List; @Data @Component @ConfigurationProperties(prefix = "recharge") public class RechargeConfig { private List tiers; @Data public static class Tier { private BigDecimal amount; // 充值金额 private Integer bonusPoints; // 赠送积分 } /** 计算赠送积分 */ public Integer calcBonusPoints(BigDecimal amount) { return tiers.stream() .filter(t -> amount.compareTo(t.getAmount()) >= 0) .mapToInt(Tier::getBonusPoints) .max().orElse(0); } /** 计算到账总积分(充值金额换算为积分 + 赠送) */ public Integer calcTotalPoints(BigDecimal amount) { int base = amount.multiply(BigDecimal.valueOf(100)).intValue(); // 1元=100积分 return base + calcBonusPoints(amount); } } ``` ```yaml # application.yml 充值配置 recharge: tiers: - amount: 10 bonusPoints: 10 - amount: 50 bonusPoints: 60 - amount: 100 bonusPoints: 150 - amount: 500 bonusPoints: 800 - amount: 1000 bonusPoints: 2000 ``` ## 四、Service 接口 + 实现 ### PaymentService.java ```java package com.openclaw.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.openclaw.dto.RechargeDTO; import com.openclaw.vo.*; public interface PaymentService { /** 发起充值,返回支付参数 */ RechargeVO createRecharge(Long userId, RechargeDTO dto); /** 微信支付回调 */ void handleWechatCallback(String xmlBody); /** 支付宝支付回调 */ void handleAlipayCallback(String body); /** 查询充值记录 */ IPage getPaymentRecords(Long userId, int pageNum, int pageSize); } ``` ### PaymentServiceImpl.java ```java package com.openclaw.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.openclaw.config.RechargeConfig; import com.openclaw.dto.RechargeDTO; import com.openclaw.entity.*; import com.openclaw.repository.*; import com.openclaw.service.*; import com.openclaw.util.IdGenerator; import com.openclaw.vo.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Slf4j @Service @RequiredArgsConstructor public class PaymentServiceImpl implements PaymentService { private final RechargeOrderRepository rechargeRepo; private final PaymentRecordRepository paymentRecordRepo; private final PointsService pointsService; private final OrderService orderService; private final RechargeConfig rechargeConfig; private final IdGenerator idGenerator; // private final WechatPayClient wechatPayClient; // 微信支付SDK // private final AlipayClient alipayClient; // 支付宝SDK @Override @Transactional public RechargeVO createRecharge(Long userId, RechargeDTO dto) { int bonus = rechargeConfig.calcBonusPoints(dto.getAmount()); int total = rechargeConfig.calcTotalPoints(dto.getAmount()); RechargeOrder order = new RechargeOrder(); order.setRechargeNo(idGenerator.generateRechargeNo()); order.setUserId(userId); order.setAmount(dto.getAmount()); order.setBonusPoints(bonus); order.setTotalPoints(total); order.setPaymentMethod(dto.getPaymentMethod()); order.setStatus("pending"); rechargeRepo.insert(order); // TODO: 调用微信/支付宝SDK生成支付参数 // String payParams = wechatPayClient.createPayOrder(...); String payParams = "{\"prepay_id\":\"mock_prepay_id\"}"; RechargeVO vo = new RechargeVO(); vo.setRechargeId(order.getId()); vo.setRechargeNo(order.getRechargeNo()); vo.setAmount(order.getAmount()); vo.setBonusPoints(bonus); vo.setTotalPoints(total); vo.setPayParams(payParams); return vo; } @Override @Transactional public void handleWechatCallback(String xmlBody) { // 1. 解析微信回调XML // 2. 验签 // 3. 查找充值订单 // 4. 幂等校验(已处理则直接返回) // 5. 更新充值订单状态 // 6. 发放积分 log.info("收到微信支付回调: {}", xmlBody); // 示例:解析 rechargeNo 后调用 completeRecharge // String rechargeNo = parseXml(xmlBody, "out_trade_no"); // String transactionId = parseXml(xmlBody, "transaction_id"); // completeRecharge(rechargeNo, transactionId); } @Override @Transactional public void handleAlipayCallback(String body) { log.info("收到支付宝回调: {}", body); // 同上,解析参数后调用 completeRecharge } /** 充值完成:更新状态 + 发放积分 */ private void completeRecharge(String rechargeNo, String transactionId) { RechargeOrder order = rechargeRepo.findByRechargeNo(rechargeNo); if (order == null || "paid".equals(order.getStatus())) return; // 幂等 order.setStatus("paid"); order.setTransactionId(transactionId); import java.time.LocalDateTime; order.setPaidAt(LocalDateTime.now()); rechargeRepo.updateById(order); // 发放积分(充值赠送) pointsService.earnPoints(order.getUserId(), "recharge", order.getId(), "recharge"); // 注意:earnPoints 里按规则取积分数,但充值积分数量是动态的,需要特殊处理 // 可以直接调用底层方法传入 totalPoints } @Override public IPage getPaymentRecords(Long userId, int pageNum, int pageSize) { IPage page = paymentRecordRepo.selectPage( new Page<>(pageNum, pageSize), new LambdaQueryWrapper() .eq(PaymentRecord::getUserId, userId) .orderByDesc(PaymentRecord::getCreatedAt)); return page.convert(r -> { PaymentRecordVO vo = new PaymentRecordVO(); vo.setId(r.getId()); vo.setBizType(r.getBizType()); vo.setBizNo(r.getBizNo()); vo.setAmount(r.getAmount()); vo.setPaymentMethod(r.getPaymentMethod()); vo.setStatus(r.getStatus()); vo.setCreatedAt(r.getCreatedAt()); return vo; }); } } ``` ## 五、Controller ### PaymentController.java ```java package com.openclaw.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.openclaw.common.Result; import com.openclaw.dto.RechargeDTO; import com.openclaw.service.PaymentService; import com.openclaw.util.UserContext; import com.openclaw.vo.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.stream.Collectors; @RestController @RequestMapping("/api/v1/payments") @RequiredArgsConstructor public class PaymentController { private final PaymentService paymentService; /** 发起充值 */ @PostMapping("/recharge") public Result createRecharge(@Valid @RequestBody RechargeDTO dto) { return Result.ok(paymentService.createRecharge(UserContext.getUserId(), dto)); } /** 微信支付回调(无需登录) */ @PostMapping("/callback/wechat") public String wechatCallback(HttpServletRequest request) throws Exception { String body = new BufferedReader(new InputStreamReader(request.getInputStream())) .lines().collect(Collectors.joining("\n")); paymentService.handleWechatCallback(body); return ""; } /** 支付宝回调(无需登录) */ @PostMapping("/callback/alipay") public String alipayCallback(HttpServletRequest request) throws Exception { String body = new BufferedReader(new InputStreamReader(request.getInputStream())) .lines().collect(Collectors.joining("\n")); paymentService.handleAlipayCallback(body); return "success"; } /** 支付记录 */ @GetMapping("/records") public Result> getRecords( @RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize) { return Result.ok(paymentService.getPaymentRecords(UserContext.getUserId(), pageNum, pageSize)); } } ``` --- **文档版本**:v1.0 | **创建日期**:2026-03-16