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

12 KiB
Raw Blame History

支付服务开发文档

一、Entity 实体类

RechargeOrder.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

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

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

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

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;
}

三、充值赠送规则配置

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);
    }
}
# 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

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

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

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