Files
number/后端架构设计/08-支付服务开发文档.md

398 lines
12 KiB
Markdown
Raw Normal View History

2026-03-17 12:09:43 +08:00
# 支付服务开发文档
## 一、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