第8章 支付.md 38 KB

谷粒随享

第8章 微信支付充值业务

学习目标:

  • 微信支付(对接第三方支付平台)
  • 基于微信支付完成订单付款
  • 基于微信支付完成充值业务

1、微信支付

1.1 保存交易记录

当用户进行订单、充值 微信支付为每笔交易产生一条本地交易记录,将来用于对账。

service-payment模块中增加保存本地交易业务处理

PaymentInfoService

package com.atguigu.tingshu.payment.service;

import com.atguigu.tingshu.model.payment.PaymentInfo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface PaymentInfoService extends IService<PaymentInfo> {

    /**
     * 保存本地交易记录
     *
     * @param paymentType 支付类型
     * @param orderNo     订单编号
     * @param userId      用户ID
     * @return
     */
    PaymentInfo savePaymentInfo(String paymentType, String orderNo, Long userId);
}

PaymentInfoServiceImpl实现类

package com.atguigu.tingshu.payment.service.impl;

import com.atguigu.tingshu.account.AccountFeignClient;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.model.account.RechargeInfo;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.atguigu.tingshu.model.payment.PaymentInfo;
import com.atguigu.tingshu.order.client.OrderFeignClient;
import com.atguigu.tingshu.payment.mapper.PaymentInfoMapper;
import com.atguigu.tingshu.payment.service.PaymentInfoService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@SuppressWarnings({"all"})
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {

    //@Autowired
    //private PaymentInfoMapper paymentInfoMapper;

    @Autowired
    private OrderFeignClient orderFeignClient;

    @Autowired
    private AccountFeignClient accountFeignClient;


    /**
     * 保存本地交易记录
     *
     * @param paymentType 支付类型:1301-订单 1302-充值
     * @param orderNo     订单编号
     * @param userId      用户ID
     * @return
     */
    @Override
    public PaymentInfo savePaymentInfo(String paymentType, String orderNo, Long userId) {
        //1.根据订单号查询本地交易记录,如果存在则直接返回
        LambdaQueryWrapper<PaymentInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(PaymentInfo::getOrderNo, orderNo);
        PaymentInfo paymentInfo = this.getOne(queryWrapper);
        if (paymentInfo != null) {
            return paymentInfo;
        }
        //2.构建本地交易记录保存-设置每个属性赋值
        paymentInfo = new PaymentInfo();
        //2.1 判断支付类型-处理订单-查询订单信息得到金额,内容
        if (SystemConstant.PAYMENT_TYPE_ORDER.equals(paymentType)) {
            //远程调用订单服务获取订单信息
            OrderInfo orderInfo = orderFeignClient.getOrderInfo(orderNo).getData();
            if (!SystemConstant.ORDER_STATUS_UNPAID.equals(orderInfo.getOrderStatus())) {
                throw new GuiguException(ResultCodeEnum.ARGUMENT_VALID_ERROR);
            }
            paymentInfo.setAmount(orderInfo.getOrderAmount());
            paymentInfo.setContent(orderInfo.getOrderTitle());
        } else if (SystemConstant.PAYMENT_TYPE_RECHARGE.equals(paymentType)) {
            //2.2 判断支付类型-处理充值-查询充值信息得到金额
            //远程调用“账户”服务获取充值记录
            RechargeInfo rechargeInfo = accountFeignClient.getRechargeInfoByOrderNo(orderNo).getData();
            if(!SystemConstant.ORDER_STATUS_UNPAID.equals(rechargeInfo.getRechargeStatus())){
                throw new GuiguException(ResultCodeEnum.ARGUMENT_VALID_ERROR);
            }
            paymentInfo.setAmount(rechargeInfo.getRechargeAmount());
            paymentInfo.setContent("充值");
        }
        paymentInfo.setUserId(userId);
        paymentInfo.setPaymentType(paymentType);
        paymentInfo.setOrderNo(orderNo);
        paymentInfo.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN);
        paymentInfo.setPaymentStatus(SystemConstant.PAYMENT_STATUS_UNPAID);
        //支付平台端交易编号-得到支付平台支付结果
        //paymentInfo.setOutTradeNo();
        //paymentInfo.setCallbackTime();
        //paymentInfo.setCallbackContent();
        this.save(paymentInfo);
        return paymentInfo;
    }
}

1.1.1 获取订单对象

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/100

service-order 订单微服务已经有这个方法了,直接编写一个远程调用即可

package com.atguigu.tingshu.order.client;

import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.atguigu.tingshu.order.client.impl.OrderDegradeFeignClient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * <p>
 * 订单模块远程调用API接口
 * </p>
 *
 * @author atguigu
 */
@FeignClient(value = "service-order", path = "/api/order", fallback = OrderDegradeFeignClient.class)
public interface OrderFeignClient {

    /**
     * 查询当前用户指定订单信息
     *
     * @param orderNo
     * @return
     */
    @GetMapping("/orderInfo/getOrderInfo/{orderNo}")
    public Result<OrderInfo> getOrderInfo(@PathVariable("orderNo") String orderNo);

}

熔断类:

package com.atguigu.tingshu.order.client.impl;


import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.atguigu.tingshu.order.client.OrderFeignClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class OrderDegradeFeignClient implements OrderFeignClient {

    @Override
    public Result<OrderInfo> getOrderInfo(String orderNo) {
        log.error("[订单服务]getOrderInfo执行服务降级");
        return null;
    }
}

1.1.2 获取充值记录信息

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/101`service-account` 微服务控制器中添加

package com.atguigu.tingshu.account.api;

import com.atguigu.tingshu.account.service.RechargeInfoService;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.account.RechargeInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "充值管理")
@RestController
@RequestMapping("api/account")
@SuppressWarnings({"all"})
public class RechargeInfoApiController {

	@Autowired
	private RechargeInfoService rechargeInfoService;


	/**
	 * 根据订单号获取充值信息
	 * @param orderNo
	 * @return
	 */
	@Operation(summary = "根据订单号获取充值信息")
	@GetMapping("/rechargeInfo/getRechargeInfo/{orderNo}")
	public Result<RechargeInfo> getRechargeInfoByOrderNo(@PathVariable String orderNo){
		RechargeInfo rechargeInfo = rechargeInfoService.getRechargeInfoByOrderNo(orderNo);
		return Result.ok(rechargeInfo);
	}
}

接口与实现

package com.atguigu.tingshu.account.service;

import com.atguigu.tingshu.model.account.RechargeInfo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface RechargeInfoService extends IService<RechargeInfo> {

    /**
     * 根据订单号获取充值信息
     * @param orderNo
     * @return
     */
    RechargeInfo getRechargeInfoByOrderNo(String orderNo);
}

package com.atguigu.tingshu.account.service.impl;

import com.atguigu.tingshu.account.mapper.RechargeInfoMapper;
import com.atguigu.tingshu.account.service.RechargeInfoService;
import com.atguigu.tingshu.model.account.RechargeInfo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@SuppressWarnings({"all"})
public class RechargeInfoServiceImpl extends ServiceImpl<RechargeInfoMapper, RechargeInfo> implements RechargeInfoService {

    @Autowired
    private RechargeInfoMapper rechargeInfoMapper;


    /**
     * 根据订单号获取充值信息
     *
     * @param orderNo
     * @return
     */
    @Override
    public RechargeInfo getRechargeInfoByOrderNo(String orderNo) {
        LambdaQueryWrapper<RechargeInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(RechargeInfo::getOrderNo, orderNo);
        return rechargeInfoMapper.selectOne(queryWrapper);
    }
}

远程调用模块service-account-clientAccountFeignClient增加远程调用方法

/**
 * 根据订单号获取充值信息
 * @param orderNo
 * @return
 */
@GetMapping("/rechargeInfo/getRechargeInfo/{orderNo}")
public Result<RechargeInfo> getRechargeInfoByOrderNo(@PathVariable String orderNo);

AccountDegradeFeignClient熔断类:

@Override
public Result<RechargeInfo> getRechargeInfoByOrderNo(String orderNo) {
    log.error("[账户]服务调用getRechargeInfoByOrderNo执行服务降级");
    return null;
}

1.2 对接微信支付

异步回调需要在微信端配置,并且只能配置一个接口地址,而现在这个接口地址有其他项目在使用。所以我们在前端模拟了一下回调功能,主动查询一下结果!

接收前端传递的支付类型以及订单编号

先在配置文件中添加支付相关信息配置:

spring.application.name=service-payment
spring.profiles.active=dev
spring.main.allow-bean-definition-overriding=true
spring.cloud.nacos.discovery.server-addr=192.168.200.130:8848
spring.cloud.nacos.config.server-addr=192.168.200.130:8848
spring.cloud.nacos.config.prefix=${spring.application.name}
spring.cloud.nacos.config.file-extension=yaml
wechat.v3pay.appid=wxcc651fcbab275e33
wechat.v3pay.merchantId=1631833859
wechat.v3pay.privateKeyPath=E:\\tingshu\\apiclient_key.pem
wechat.v3pay.merchantSerialNumber=4AE80B52EBEAB2B96F68E02510A42801E952E889
wechat.v3pay.apiV3key=84dba6dd51cdaf779e55bcabae564b53
wechat.v3pay.notifyUrl=http://127.0.0.1:8500/api/payment/wxPay/notify

apiclient_key.pem 商户API私钥路径 的私钥要放入指定位置,让程序读取!

注意:RSAAutoCertificateConfig 对象必须在配置文件中注入到spring 容器中,不要直接使用原生的,否则会报错!

package com.atguigu.tingshu.payment.config;

import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix="wechat.v3pay") //读取节点
@Data
public class WxPayV3Config {

    private String appid;
    /** 商户号 */
    public String merchantId;
    /** 商户API私钥路径 */
    public String privateKeyPath;
    /** 商户证书序列号 */
    public String merchantSerialNumber;
    /** 商户APIV3密钥 */
    public String apiV3key;
    /** 回调地址 */
    private String notifyUrl;

    @Bean
    public RSAAutoCertificateConfig rsaAutoCertificateConfig(){
        return new RSAAutoCertificateConfig.Builder()
                .merchantId(this.merchantId)
                .privateKeyFromPath(privateKeyPath)
                .merchantSerialNumber(merchantSerialNumber)
                .apiV3Key(apiV3key)
                .build();
    }

    /**
     * 调用JSAPI-小程序、APP场景使用的服务类对象
     * @return
     */
    @Bean
    public JsapiServiceExtension service(){
        return new JsapiServiceExtension.Builder().config(rsaAutoCertificateConfig()).build();
    }
}

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/102

1.2.1 支付控制器

package com.atguigu.tingshu.payment.api;

import com.atguigu.tingshu.common.login.GuiGuLogin;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.payment.service.WxPayService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@Tag(name = "微信支付接口")
@RestController
@RequestMapping("api/payment")
@Slf4j
public class WxPayApiController {

    @Autowired
    private WxPayService wxPayService;


    /**
     * 微信支付
     *
     * @param paymentType 支付类型:1301-订单 1302-充值
     * @param orderNo     订单号
     * @return
     */
    @GuiGuLogin
    @Operation(summary = "微信下单")
    @PostMapping("/wxPay/createJsapi/{paymentType}/{orderNo}")
    public Result<Map<String, String>> createJsapi(@PathVariable String paymentType, @PathVariable String orderNo) {
        Map<String, String> wxPayResult = wxPayService.createJsapiWxPayForm(paymentType, orderNo);
        return Result.ok(wxPayResult);
    }
}

1.2.2 支付接口

package com.atguigu.tingshu.payment.service;

import java.util.Map;

public interface WxPayService {

    /**
     * 调用微信支付API响应给小程序能够拉起微信支付页面参数
     *
     * @param paymentType
     * @param orderNo
     * @return
     */
    Map<String, String> createJsapiWxPayForm(String paymentType, String orderNo);
}

1.2.3 支付实现类

微信支付:小程序调起支付需要返回的参数

支付文档入口:https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestPayment.html 对接方式:https://github.com/wechatpay-apiv3/wechatpay-java

package com.atguigu.tingshu.payment.service.impl;

import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.atguigu.tingshu.model.payment.PaymentInfo;
import com.atguigu.tingshu.order.client.OrderFeignClient;
import com.atguigu.tingshu.payment.config.WxPayV3Config;
import com.atguigu.tingshu.payment.service.PaymentInfoService;
import com.atguigu.tingshu.payment.service.WxPayService;
import com.atguigu.tingshu.user.client.UserFeignClient;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.payments.jsapi.model.Payer;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

    @Autowired
    private PaymentInfoService paymentInfoService;

    @Autowired
    private OrderFeignClient orderFeignClient;

    @Autowired
    private WxPayV3Config wxPayV3Config;


    @Autowired
    private JsapiServiceExtension service;

    @Autowired
    private UserFeignClient userFeignClient;


    /**
     * 调用微信支付API响应给小程序能够拉起微信支付页面参数
     *
     * @param paymentType 支付方式:微信
     * @param orderNo     订单编号
     * @return
     */
    @Override
    public Map<String, String> createJsapiWxPayForm(String paymentType, String orderNo) {
        try {
            //1.新增本地交易记录
            Long userId = AuthContextHolder.getUserId();
            PaymentInfo paymentInfo = paymentInfoService.savePaymentInfo(paymentType, orderNo, userId);
            //2.调用微信API返回拉起微信支付页面参数
            //3.1 调用微信预交易的请求对象 单位:分
            PrepayRequest request = new PrepayRequest();
            Amount amount = new Amount();
            amount.setTotal(1); //预交易订单金额 1分
            request.setAmount(amount);
            request.setAppid(wxPayV3Config.getAppid());
            request.setMchid(wxPayV3Config.getMerchantId());
            request.setDescription(paymentInfo.getContent());
            request.setNotifyUrl(wxPayV3Config.getNotifyUrl());
            request.setOutTradeNo(orderNo);
            //小程序开发阶段-只有小程序开发者才能进行付款 购买者参数上线有不需要设置
            UserInfoVo userInfoVo = userFeignClient.getUserInfoVoByUserId(userId).getData();
            Payer payer = new Payer();
            payer.setOpenid(userInfoVo.getWxOpenId());
            request.setPayer(payer);
            PrepayWithRequestPaymentResponse paymentResponse = service.prepayWithRequestPayment(request);
            if (paymentResponse != null) {
                Map<String, String> map = new HashMap<>();
                map.put("timeStamp", paymentResponse.getTimeStamp());
                map.put("package", paymentResponse.getPackageVal());
                map.put("paySign", paymentResponse.getPaySign());
                map.put("signType", paymentResponse.getSignType());
                map.put("nonceStr", paymentResponse.getNonceStr());
                return map;
            }
        } catch (Exception e) {
            log.error("[支付]微信预支付创建失败:{}", e);
            throw new RuntimeException(e);
        }
        return null;
    }
}

1.3 查询支付状态(同步)

需求:当用户微信付款后,在小程序端需要通过查询微信端交易状态,如果用户已支付,给小程序端响应结果,展示支付成功页面。

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/103

接口说明:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_2.shtml

WxPayApiController控制器:

/**
 * 根据商户订单编号,支付微信支付支付状态
 *
 * @param orderNo
 * @return
 */
@Operation(summary = "根据商户订单编号,支付微信支付支付状态")
@GetMapping("/wxPay/queryPayStatus/{orderNo}")
public Result<Boolean> queryPayStatus(@PathVariable String orderNo) {
    Boolean status = wxPayService.queryPayStatus(orderNo);
    return Result.ok(status);
}

WxPayService接口:

/**
 * 根据商户订单编号,支付微信支付支付状态
 *
 * @param orderNo
 * @return
 */
Boolean queryPayStatus(String orderNo);

WxPayServiceImpl实现类:

/**
 * 根据商户订单编号,支付微信支付支付状态
 *
 * @param orderNo
 * @return
 */
@Override
public Boolean queryPayStatus(String orderNo) {
    try {
        //1.构建微信交易结果查询对象-按照订单编号查询
        QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
        request.setMchid(wxPayV3Config.getMerchantId());
        request.setOutTradeNo(orderNo);
        //2.发送请求查询微信交易结果
        Transaction transaction = service.queryOrderByOutTradeNo(request);

        //3.解析结果得到支付状态
        if (transaction != null) {
            Transaction.TradeStateEnum tradeState = transaction.getTradeState();
            if (Transaction.TradeStateEnum.SUCCESS == tradeState) {
                //说明用户支付成功
                return true;
            }
        }
    } catch (Exception e) {
        log.error("[支付]查询微信交易状态异常:{}", e);
        throw new RuntimeException(e);
    }
    return false;
}

1.4 支付异步回调

异步回调说明:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml

内网穿透工具:https://natapp.cn/

1.4.1 支付服务-处理异步回调

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/104

WxPayApiController控制器:

/**
 * 异步回调
 *
 * @param request
 * @return
 */
@Operation(summary = "微信支付异步通知接口")
@PostMapping("/wxPay/notify")
public Map<String, String> notify(HttpServletRequest request) {
    Map<String, String> map = wxPayService.notifyTractionStatus(request);
    return map;
}

WxPayService接口

/**
 * 处理微信异步回调:支付结果
 * @param request
 * @return
 */
Map<String, String> notifyTractionStatus(HttpServletRequest request);

WxPayServiceImpl实现类

/**
 * 处理微信异步回调:支付结果
 *
 * @param request
 * @return
 */
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, String> notifyTractionStatus(HttpServletRequest request) {
    try {
        //1.验证签名 验证数据是否被篡改 避免“假通知”出现
        //1.1 通过请求头获取原始报文
        String wechatPaySerial = request.getHeader("Wechatpay-Serial");  //签名
        String nonce = request.getHeader("Wechatpay-Nonce");  //签名中的随机数
        String timestamp = request.getHeader("Wechatpay-Timestamp"); //时间戳
        String signature = request.getHeader("Wechatpay-Signature"); //签名类型
        log.info("wechatPaySerial:{}", wechatPaySerial);
        log.info("nonce:{}", nonce);
        log.info("timestamp:{}", timestamp);
        log.info("signature:{}", signature);

        //获取请求对象中 原始所有的请求头参数
        String requestBody = PayUtil.readData(request);
        // 构造 RequestParam
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(wechatPaySerial)
                .nonce(nonce)
                .signature(signature)
                .timestamp(timestamp)
                .body(requestBody)
                .build();

        // 如果已经初始化了 RSAAutoCertificateConfig,可直接使用
        //1.2 NotificationParser 用于真正验签对象
        NotificationParser parser = new NotificationParser(config);

        //2.验签通过后 获取支付结果
        // 以支付通知回调为例,验签、解密并转换成 Transaction
        Transaction transaction = parser.parse(requestParam, Transaction.class);
        if (transaction != null && transaction.getTradeState() == Transaction.TradeStateEnum.SUCCESS) {
            //TODO 3.根据支付结果处理后续业务:本地交易记录、订单、购买记录 -------      充值
            paymentInfoService.updatePaymentInfo(transaction.getOutTradeNo());
            Map<String, String> map = new HashMap<>();
            map.put("code", "SUCCESS");
            map.put("message", "SUCCESS");
            return map;
        }
    } catch (Exception e) {
        log.error("[支付]获取支付结果异常:{}", e);
        throw new RuntimeException(e);
    }
    Map<String, String> map = new HashMap<>();
    map.put("code", "FAIL");
    map.put("message", "FAIL");
    return map;
}

本地交易记录变更

PaymentInfoService

/**
 * 更新本地交易记录状态:改为支付成功
 * @param orderNo
 */
void updatePaymentInfo(String orderNo);

PaymentInfoServiceImpl

/**
 * 更新本地交易记录、异步基于MQ更新订单状态、充值、购买记录
 *
 * @param orderNo 商户的订单编号
 */
@Override
public void updatePaymentInfo(String orderNo) {
    //1.根据订单编号查询本地交易记录,如果本地交易记录已支付(说明更新过了,不处理)
    LambdaQueryWrapper<PaymentInfo> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(PaymentInfo::getOrderNo, orderNo);
    PaymentInfo paymentInfo = this.getOne(queryWrapper);
    if (paymentInfo != null && SystemConstant.PAYMENT_STATUS_PAID.equals(paymentInfo.getPaymentStatus())) {
        return;
    }

    //更新本地交易记录状态
    paymentInfo.setPaymentStatus(SystemConstant.PAYMENT_STATUS_PAID);
    this.updateById(paymentInfo);

    //2.利用MQ更新订单支付状态、充值记录状态
    //2.1 从本地交易记录获取 支付类型:1301-订单 1302-充值 动态得到目标话题名称
    String topic =
            paymentInfo.getPaymentType().equals(SystemConstant.PAYMENT_TYPE_ORDER) ? KafkaConstant.QUEUE_ORDER_PAY_SUCCESS : KafkaConstant.QUEUE_RECHARGE_PAY_SUCCESS;
    //2.2 将消息发送指定话题中
    kafkaService.sendMessage(topic, orderNo);
}

1.4.2 订单服务监听-更新订单状态

所有支付成功订单编号被发送到:tingshu.order.pay.success

service-order提供消费者OrderReceiver 监听处理被成功支付订单:

package com.atguigu.tingshu.order.reciever;

import com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.order.service.OrderInfoService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

/**
 * @author: atguigu
 * @create: 2023-11-07 15:01
 */
@Slf4j
@Component
public class OrderReceiver {

    @Autowired
    private OrderInfoService orderInfoService;

    /**
     * 监听为在线支持成功的订单,修改订单状态
     *
     * @param record
     */
    @KafkaListener(topics = KafkaConstant.QUEUE_ORDER_PAY_SUCCESS)
    public void orderPaySuccess(ConsumerRecord<String, String> record) {
        String orderNo = record.value();
        if (StringUtils.isNotBlank(orderNo)) {
            orderInfoService.orderPaySuccess(orderNo);
        }
    }


}

OrderInfoService

/**
 * 更新订单状态:已支付、基于MQ异步新增用户购买记录
 * @param orderNo
 */
void orderPaySuccess(String orderNo);

OrderInfoServiceImpl

/**
 * 更新订单状态:已支付、基于MQ异步新增用户购买记录
 *
 * @param orderNo
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void orderPaySuccess(String orderNo) {
    //1.根据订单编号查询订单信息,更新订单状态:已支付(幂等性)
    OrderInfo orderInfo = orderInfoMapper.selectOne(new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo, orderNo));
    orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID);
    orderInfoMapper.updateById(orderInfo);

    //2.发送MQ消息通知“用户服务”新增用户购买记录 UserPaidRecordVo
    UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo();
    userPaidRecordVo.setOrderNo(orderNo);
    userPaidRecordVo.setUserId(orderInfo.getUserId());
    userPaidRecordVo.setItemType(orderInfo.getItemType());
    //获取订单明细 封装用户购买项ID集合
    List<OrderDetail> orderDetailList = orderDetailMapper.selectList(new LambdaQueryWrapper<OrderDetail>().eq(OrderDetail::getOrderId, orderInfo.getId()));
    if(CollectionUtil.isNotEmpty(orderDetailList)){
        List<Long> itemIdList = orderDetailList.stream().map(OrderDetail::getItemId).collect(Collectors.toList());
        userPaidRecordVo.setItemIdList(itemIdList);
        kafkaService.sendMessage(KafkaConstant.QUEUE_USER_PAY_RECORD, JSON.toJSONString(userPaidRecordVo));
    }
}

1.4.3 用户服务监听-新增购买记录

service-user用户模块消费者:UserReceiver 已完成新增购买记录(直接复用即可)

2、充值业务

点击我的---->我的钱包

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/105

service-account 模块中添加控制器

前端传递充值金额与充值方式,所以我们需要封装一个实体类接受前端传递的数据.

实体类RechargeInfoVo

@Data
@Schema(description = "充值对象")
public class RechargeInfoVo {

   @Schema(description = "充值金额")
   private BigDecimal amount;

   @Schema(description = "支付方式:1101-微信 1102-支付宝")
   private String payWay;

}

service-account模块中控制器:RechargeInfoApiController

/**
 * 用户余额充值(保存充值记录)
 *
 * @param rechargeInfoVo
 * @return
 */
@GuiGuLogin
@Operation(summary = "用户余额充值-保存充值记录")
@PostMapping("/rechargeInfo/submitRecharge")
public Result<Map<String, String>> submitRecharge(@RequestBody RechargeInfoVo rechargeInfoVo) {
    Map<String, String> map = rechargeInfoService.submitRecharge(rechargeInfoVo);
    return Result.ok(map);
}

RechargeInfoService接口:

/**
 * 用户余额充值(保存充值记录)
 *
 * @param rechargeInfoVo
 * @return
 */
Map<String, String> submitRecharge(RechargeInfoVo rechargeInfoVo);

实现类:

/**
 * 用户余额充值(保存充值记录)
 *
 * @param rechargeInfoVo
 * @return
 */
@Override
public Map<String, String> submitRecharge(RechargeInfoVo rechargeInfoVo) {
    //1.为本次充值记录生成订单编号-保存充值记录(未支付)
    RechargeInfo rechargeInfo = new RechargeInfo();
    rechargeInfo.setUserId(AuthContextHolder.getUserId());
    rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_UNPAID);
    rechargeInfo.setRechargeAmount(rechargeInfoVo.getAmount());
    rechargeInfo.setPayWay(rechargeInfoVo.getPayWay());
    String orderNo = "CZ" + DateUtil.today().replaceAll("-", "") + IdUtil.getSnowflakeNextId();
    rechargeInfo.setOrderNo(orderNo);
    rechargeInfoMapper.insert(rechargeInfo);

    //3.TODO 利用延迟队列,延迟关闭充值记录
    Map<String, String> map = new HashMap<>();
    map.put("orderNo", orderNo);
    return map;
}

2.1 充值成功业务处理

在支付成功之后,会在支付服务中提供异步回调处理支付成功业务。

  • 发送充值成功消息到 tingshu.recharge.pay.success
  • service-account 模块添加监听器处理充值成功的充值订单业务即可

2.1.1 监听信息修改充值状态

在这个AccountReceiver类中添加数据

@Autowired
private RechargeInfoService rechargeInfoService;

/**
 * 监听用户充值成功消息
 *
 * @param record
 */
@KafkaListener(topics = KafkaConstant.QUEUE_RECHARGE_PAY_SUCCESS)
public void rechargeSuccess(ConsumerRecord<String, String> record) {
    String orderNo = record.value();
    if (StringUtils.isNotBlank(orderNo)) {
        log.info("[账户服务]监听到充值成功消息,订单编号:{}", orderNo);
        rechargeInfoService.rechargeSuccess(orderNo);
    }
}

RechargeInfoService接口

/**
 * 处理充值成功消息
 * @param orderNo
 */
void rechargeSuccess(String orderNo);

RechargeInfoServiceImpl实现

/**
 * 处理充值成功消息
 *
 * @param orderNo
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void rechargeSuccess(String orderNo) {
    //1.根据充值订单编号查询充值记录状态
    RechargeInfo rechargeInfo = rechargeInfoMapper.selectOne(new LambdaQueryWrapper<RechargeInfo>().eq(RechargeInfo::getOrderNo, orderNo));
    if (rechargeInfo != null && SystemConstant.ORDER_STATUS_UNPAID.equals(rechargeInfo.getRechargeStatus())) {
        //只处理未支付
        rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_PAID);
        rechargeInfoMapper.updateById(rechargeInfo);
        //2.修改账户总金额、可用余额
        userAccountService.add(rechargeInfo.getUserId(), rechargeInfo.getRechargeAmount());

        //3.新增账户变动记录
        userAccountService.saveUserAccountDetail(rechargeInfo.getUserId(), "充值", SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT, rechargeInfo.getRechargeAmount(), rechargeInfo.getOrderNo());
    }
}

2.1.2 账户充值

UserAccountService

新增 user_account 与 user_account_detail 表数据

/**
 * 为指定账户充值
 * @param userId
 * @param rechargeAmount
 */
void add(Long userId, BigDecimal rechargeAmount);
/**
 * 为指定账户充值
 * @param userId
 * @param rechargeAmount
 */
@Override
public void add(Long userId, BigDecimal rechargeAmount) {
    userAccountMapper.add(userId, rechargeAmount);
}

UserAccountMapper

/**
 * 充值
 * @param userId
 * @param rechargeAmount
 */
void add(@Param("userId") Long userId, @Param("amount") BigDecimal rechargeAmount);

Mapper.xml

<!--账户充值-->
<insert id="add">
    UPDATE user_account set total_amount = total_amount + #{amount}, available_amount=available_amount+#{amount} ,total_income_amount = total_income_amount +#{amount} where user_id = #{userId}
</insert>

2.2 充值记录

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/106

用户充值记录回显信息保存到实体类:

package com.atguigu.tingshu.model.account;

import com.atguigu.tingshu.model.base.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.math.BigDecimal;

@Data
@Schema(description = "UserAccountDetail")
@TableName("user_account_detail")
public class UserAccountDetail extends BaseEntity {

   private static final long serialVersionUID = 1L;

   @Schema(description = "用户id")
   @TableField("user_id")
   private Long userId;

   @Schema(description = "交易标题")
   @TableField("title")
   private String title;

   @Schema(description = "交易类型:1201-充值 1202-锁定 1203-解锁 1204-消费")
   @TableField("trade_type")
   private String tradeType;

   @Schema(description = "金额")
   @TableField("amount")
   private BigDecimal amount;

   @Schema(description = "订单编号")
   @TableField("order_no")
   private String orderNo;

}

service-account 微服务 UserAccountApiController 控制器添加

/**
 * 用户充值记录
 * @param page
 * @param limit
 * @return
 */
@GuiGuLogin
@Operation(summary = "获取用户充值记录")
@GetMapping("/userAccount/findUserRechargePage/{page}/{limit}")
public Result findUserRechargePage(
      @Parameter(name = "page", description = "当前页码", required = true)
      @PathVariable Long page,

      @Parameter(name = "limit", description = "每页记录数", required = true)
      @PathVariable Long limit) {
   // 获取到用户Id
   Long userId = AuthContextHolder.getUserId();
   // 构建Page对象
   Page<UserAccountDetail> pageParam = new Page<>(page, limit);
   // 调用服务层方法
   IPage<UserAccountDetail> pageModel = userAccountService.getUserRechargePage(pageParam, userId);
   // 返回数据
   return Result.ok(pageModel);
}

接口:

/**
 * 查看用户充值记录
 * @param pageParam
 * @param userId
 * @return
 */
IPage<UserAccountDetail> findUserRechargePage(Page<UserAccountDetail> pageParam, Long userId);

实现类:

@Override
public IPage<UserAccountDetail> findUserRechargePage(Page<UserAccountDetail> pageParam, Long userId) {
   // 调用mapper 方法
   return userAccountDetailMapper.getUserRechargePage(pageParam,userId);
}
@Mapper
public interface UserAccountDetailMapper extends BaseMapper<UserAccountDetail> {
    /**
     *
     * @param pageParam
     * @param userId
     * @return
     */
    IPage<UserAccountDetail> getUserRechargePage(Page<UserAccountDetail> pageParam, Long userId);
}

xml 配置文件

<!--
   充值记录类型 固定1201
-->
<select id="getUserRechargePage" resultMap="RechargeInfoMap">
   select <include refid="columns" />
   from user_account_detail
   where user_id = #{userId} and trade_type = '1201'
   and is_deleted = 0
   order by id desc
</select>

2.3 消费记录

消费记录

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/107

在service-account 微服务中添加

@GuiGuLogin
@Operation(summary = "获取用户消费记录")
@GetMapping("/userAccount/findUserConsumePage/{page}/{limit}")
public Result getUserConsumePage(
      @Parameter(name = "page", description = "当前页码", required = true)
      @PathVariable Long page,

      @Parameter(name = "limit", description = "每页记录数", required = true)
      @PathVariable Long limit) {
   // 获取到用户Id
   Long userId = AuthContextHolder.getUserId();
   Page<UserAccountDetail> pageParam = new Page<>(page, limit);
   IPage<UserAccountDetail> pageModel = userAccountService.getUserConsumePage(pageParam, userId);
   return Result.ok(pageModel);
}

接口:

/**
 * 消费记录
 * @param pageParam
 * @param userId
 * @return
 */
IPage<UserAccountDetail> getUserConsumePage(Page<UserAccountDetail> pageParam, Long userId);

实现类:

@Override
public IPage<UserAccountDetail> getUserConsumePage(Page<UserAccountDetail> pageParam, Long userId) {
   // 调用mapper 层方法
   return userAccountDetailMapper.selectUserConsumePage(pageParam, userId);
}

Mapper

/**
 * 消费记录
 * @param pageParam
 * @param userId
 * @return
 */
IPage<UserAccountDetail> getUserConsumePage(Page<UserAccountDetail> pageParam, Long userId);

xml

<select id="getUserConsumePage" resultMap="RechargeInfoMap">
   select <include refid="columns" />
   from user_account_detail
   where user_id = #{userId} and trade_type = '1204'
   and is_deleted = 0
   order by id desc
</select>

测试:充值一百元之后,查看余额与

充值记录: