Files
number/后端架构设计/08-支付服务开发文档.md
2026-03-17 12:09:43 +08:00

398 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 支付服务开发文档
## 一、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<Tier> 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<PaymentRecordVO> 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<PaymentRecordVO> getPaymentRecords(Long userId, int pageNum, int pageSize) {
IPage<PaymentRecord> page = paymentRecordRepo.selectPage(
new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<PaymentRecord>()
.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<RechargeVO> 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 "<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>";
}
/** 支付宝回调(无需登录) */
@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<IPage<PaymentRecordVO>> 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