Initial commit
This commit is contained in:
397
后端架构设计/08-支付服务开发文档.md
Normal file
397
后端架构设计/08-支付服务开发文档.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# 支付服务开发文档
|
||||
|
||||
## 一、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
|
||||
Reference in New Issue
Block a user