第8章 支付.md 46 KB

谷粒随享

第8章 微信支付充值业务

学习目标:

  • 本地交易记录保存(用于对账)
    • 下单
    • 充值
  • 微信支付(对接第三方支付平台)
  • 基于微信支付完成付款
    • 下单
    • 充值
  • 基于微信支付完成充值业务

1、微信支付

1.1 保存交易记录

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

1.1.1 获取订单对象

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

已有Restful接口实现。只需要在service-order-client模块中新增Feign远程调用方法

OrderFeignClient

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 String orderNo);

}

OrderDegradeFeignClient熔断类:

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 微服务RechargeInfoApiController控制器中添加

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> getRechargeInfo(@PathVariable String orderNo){
		RechargeInfo rechargeInfo = rechargeInfoService.getRechargeInfo(orderNo);
		return Result.ok(rechargeInfo);
	}

}

RechargeInfoService接口

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 getRechargeInfo(String orderNo);
}

RechargeInfoServiceImpl实现类

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 getRechargeInfo(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> getRechargeInfo(@PathVariable String orderNo);

AccountDegradeFeignClient熔断类:

@Override
public Result<RechargeInfo> getRechargeInfo(String orderNo) {
    log.error("[账户服务]提供远程调用接口getRechargeInfo服务降级");
    return null;
}

1.1.3 保存本地交易记录

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:支付类型 1301-订单 1302-充值
     * @param orderNo: 订单编号
     * @return
     */
    public PaymentInfo savePaymentInfo(String paymentType, String orderNo);



}

PaymentInfoServiceImpl实现类

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

import cn.hutool.core.lang.Assert;
import com.atguigu.tingshu.account.AccountFeignClient;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
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 OrderFeignClient orderFeignClient;

    @Autowired
    private AccountFeignClient accountFeignClient;


    /**
     * 保存本地交易记录
     *
     * @param paymentType:支付类型 1301-订单 1302-充值
     * @param orderNo:         订单编号
     * @param userId:          用户ID
     * @return
     */
    @Override
    public PaymentInfo savePaymentInfo(String paymentType, String orderNo) {
        //1.根据订单编号查询本地交易记录是否存在 如果存在则返回即可
        LambdaQueryWrapper<PaymentInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(PaymentInfo::getOrderNo, orderNo);
        queryWrapper.last("limit 1");
        PaymentInfo paymentInfo = this.getOne(queryWrapper);
        if (paymentInfo != null) {
            return paymentInfo;
        }
        //2.创建本地交易记录封装各项属性并且保存
        paymentInfo = new PaymentInfo();
        //2.1 对基本属性赋值
        paymentInfo.setPaymentType(paymentType);
        paymentInfo.setOrderNo(orderNo);
        paymentInfo.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN);
        //1401-未支付 1402-已支付
        paymentInfo.setPaymentStatus(SystemConstant.PAYMENT_STATUS_UNPAID);
        //判断本次交易类型是订单还是充值
        //2.2 处理交易类型是订单
        if (SystemConstant.PAYMENT_TYPE_ORDER.equals(paymentType)) {
            //2.2.1 远程调用“订单”服务获取订单信息
            OrderInfo orderInfo = orderFeignClient.getOrderInfo(orderNo).getData();
            Assert.notNull(orderInfo, "订单不存在");
            String orderStatus = orderInfo.getOrderStatus();
            //2.2.2 判断订单状态是否合法(未支付)
            Assert.state(SystemConstant.ORDER_STATUS_UNPAID.equals(orderStatus), "订单状态有误!");
            //2.2.3 封装用户ID,金额,内容
            paymentInfo.setUserId(orderInfo.getUserId());
            paymentInfo.setAmount(orderInfo.getOrderAmount());
            paymentInfo.setContent(orderInfo.getOrderTitle());
        } else if (SystemConstant.PAYMENT_TYPE_RECHARGE.equals(paymentType)) {
            //2.3 处理交易类型是充值
            //2.3.1 远程调用“账户”服务获取充值信息
            RechargeInfo rechargeInfo = accountFeignClient.getRechargeInfo(orderNo).getData();
            Assert.notNull(rechargeInfo, "充值记录不存在!");
            //2.3.2 判断充值记录状态是否合法(未支付)
            Assert.state(SystemConstant.ORDER_STATUS_UNPAID.equals(rechargeInfo.getRechargeStatus()), "充值记录状态有误");
            //2.3.3 封装用户ID,金额,内容
            paymentInfo.setUserId(rechargeInfo.getUserId());
            paymentInfo.setAmount(rechargeInfo.getRechargeAmount());
            paymentInfo.setContent("充值金额:" + rechargeInfo.getRechargeAmount());
        }
        //TODO 以下三个字段 微信端交易编号、回调内容、回调时间 再支持成功后微信回调中才能更新
        //paymentInfo.setOutTradeNo();
        //paymentInfo.setCallbackTime();
        //paymentInfo.setCallbackContent();
        this.save(paymentInfo);
        //3.返回本地交易记录
        return paymentInfo;
    }
}

1.2 对接微信支付

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

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

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

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

    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
    #TODO 确保能加载本地应用私钥文件
    wechat.v3pay.privateKeyPath=D:\\tmp\\apiclient_key.pem
    wechat.v3pay.merchantSerialNumber=4AE80B52EBEAB2B96F68E02510A42801E952E889
    wechat.v3pay.apiV3key=84dba6dd51cdaf779e55bcabae564b53
    #TODO 改为公网域名能确保微信调用成功
    wechat.v3pay.notifyUrl=http://127.0.0.1:8500/api/payment/wxPay/notify
    
  2. apiclient_key.pem 商户API私钥路径 的私钥要放入指定位置,让程序读取!

  3. 注意: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();
    }
    }
    

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>> getMiniProgramPayParams(@PathVariable String paymentType, @PathVariable String orderNo){
        Map<String, String> map = wxPayService.getMiniProgramPayParams(paymentType, orderNo);
        return Result.ok(map);
    }
}

1.2.2 支付接口

package com.atguigu.tingshu.payment.service;

import java.util.Map;

public interface WxPayService {

    /**
     * 对接微信支付获取小程序拉起微信支付所需参数,用于微信支付进行购买订单,以及充值
     * @param paymentType 支付类型:1301-订单 1302-充值
     * @param orderNo 订单编号
     * @return
     */
    Map<String, String> getMiniProgramPayParams(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.model.payment.PaymentInfo;
import com.atguigu.tingshu.payment.config.WxPayV3Config;
import com.atguigu.tingshu.payment.service.PaymentInfoService;
import com.atguigu.tingshu.payment.service.WxPayService;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
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 RSAAutoCertificateConfig rsaAutoCertificateConfig;


    @Autowired
    private WxPayV3Config wxPayV3Config;

    /**
     * 对接微信支付获取小程序拉起微信支付所需参数,用于微信支付进行购买订单,以及充值
     *
     * @param paymentType 支付类型:1301-订单 1302-充值
     * @param orderNo     订单编号
     * @return
     */
    @Override
    public Map<String, String> getMiniProgramPayParams(String paymentType, String orderNo) {
        try {
            //1.查询或保存本地交易记录(后期对账使用)
            PaymentInfo paymentInfo = paymentInfoService.savePaymentInfo(paymentType, orderNo);
            //2.判断本地交易记录状态 必须是未支付
            if (paymentInfo != null && SystemConstant.PAYMENT_STATUS_UNPAID.equals(paymentInfo.getPaymentStatus())) {
                //3.对接微信支付接口获取小程序所需支付参数
                //3.1 JSAPI支付和APP支付业务类对象
                JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(rsaAutoCertificateConfig).build();
                //3.2 创建预下单请求对象
                PrepayRequest request = new PrepayRequest();
                Amount amount = new Amount();
                //微信金额单位分:1分 所有支付金额为1分
                amount.setTotal(1);
                request.setAmount(amount);
                request.setAppid(wxPayV3Config.getAppid());
                request.setMchid(wxPayV3Config.getMerchantId());
                request.setDescription(paymentInfo.getContent());
                request.setNotifyUrl(wxPayV3Config.getNotifyUrl());
                //微信参数OutTradeNo:商户侧订单编号
                request.setOutTradeNo(paymentInfo.getOrderNo());
                //小程序未上线:微信官方只允许appId应用下开发者账户才有权限进行付款
                Payer payer = new Payer();
                payer.setOpenid("odo3j4qp-wC3HVq9Z_D9C0cOr0Zs");
                request.setPayer(payer);
                //3.3 调用微信预下单接口获取响应,解析获取五项参数
                PrepayWithRequestPaymentResponse response = service.prepayWithRequestPayment(request);
                if (response != null) {
                    String timeStamp = response.getTimeStamp();
                    String nonceStr = response.getNonceStr();
                    String packageVal = response.getPackageVal();
                    String signType = response.getSignType();
                    String paySign = response.getPaySign();
                    //4.封装五项参数返回给客户端
                    Map<String, String> map = new HashMap<>();
                    map.put("timeStamp", timeStamp);
                    map.put("nonceStr", nonceStr);
                    map.put("package", packageVal);
                    map.put("signType", signType);
                    map.put("paySign", paySign);
                    return map;
                }
            }
        } catch (Exception e) {
            log.error("[支付服务]对接微信支付失败:{}", e);
            throw new RuntimeException(e);
        }
        return null;
    }
}

1.3 查询支付状态(同步)

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

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

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

WxPayApiController控制器:

/**
 * 小程序轮询查询支付结果-根据商户订单编号查询交易状态
 *
 * @param orderNo
 * @return
 */
@Operation(summary = "小程序轮询查询支付结果-根据商户订单编号查询交易状态")
@GetMapping("/wxPay/queryPayStatus/{orderNo}")
public Result<Boolean> queryPayStatus(@PathVariable String orderNo) {
    Boolean isPay = wxPayService.queryPayStatus(orderNo);
    return Result.ok(isPay);
}

WxPayService接口:

/**
 * 根据商户订单编号查询交易状态
 * @param orderNo
 * @return
 */
Boolean queryPayStatus(String orderNo);

WxPayServiceImpl实现类:

/**
 * 根据商户订单编号查询交易状态
 *
 * @param orderNo
 * @return
 */
@Override
public Boolean queryPayStatus(String orderNo) {
    //1.创建查询交易请求对象
    QueryOrderByOutTradeNoRequest queryOrderByOutTradeNoRequest = new QueryOrderByOutTradeNoRequest();
    queryOrderByOutTradeNoRequest.setMchid(wxPayV3Config.getMerchantId());
    queryOrderByOutTradeNoRequest.setOutTradeNo(orderNo);

    //2.创建调用微信服务端业务对象
    JsapiServiceExtension jsapiService = new JsapiServiceExtension.Builder().config(rsaAutoCertificateConfig).build();

    //3.调用微信查询交易状态接口
    Transaction transaction = jsapiService.queryOrderByOutTradeNo(queryOrderByOutTradeNoRequest);

    //4.解析响应结果返回交易状态
    if (transaction != null) {
        Transaction.TradeStateEnum tradeState = transaction.getTradeState();
        if (Transaction.TradeStateEnum.SUCCESS == tradeState) {
            //用户支付成功
            return true;
        }
    }
    return false;
}

大家实现(没有办法支付,故在同步回调中模拟支付成功)-代码

/**
 * 根据商户订单编号查询交易状态
 *
 * @param orderNo
 * @return
 */
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public Boolean queryPayStatus(String orderNo) {
    //1.创建查询交易请求对象
    //QueryOrderByOutTradeNoRequest queryOrderByOutTradeNoRequest = new QueryOrderByOutTradeNoRequest();
    //queryOrderByOutTradeNoRequest.setMchid(wxPayV3Config.getMerchantId());
    //queryOrderByOutTradeNoRequest.setOutTradeNo(orderNo);
    //
    ////2.创建调用微信服务端业务对象
    //JsapiServiceExtension jsapiService = new JsapiServiceExtension.Builder().config(rsaAutoCertificateConfig).build();
    //
    ////3.调用微信查询交易状态接口
    //Transaction transaction = jsapiService.queryOrderByOutTradeNo(queryOrderByOutTradeNoRequest);
    //
    ////4.解析响应结果返回交易状态
    //if (transaction != null) {
    //    Transaction.TradeStateEnum tradeState = transaction.getTradeState();
    //    if (Transaction.TradeStateEnum.SUCCESS == tradeState) {
    //        //用户支付成功
    //        return true;
    //    }
    //}
    //return false;

    //1.伪造微信交易对象
    Transaction transaction = new Transaction();
    transaction.setOutTradeNo(orderNo);
    transaction.setTransactionId("WX"+ IdUtil.getSnowflakeNextId());
    paymentInfoService.updatePaymentInfoSuccess(transaction);
    return true;
}

1.4 支付异步回调

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

1.4.1 内网穿透工具

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

  1. 注册账户,实名认证

  2. 购买免费隧道,设置域名映射本地IP及端口

    image-20231204105254982

  3. 修改netapp程序配置文件

    image-20231204105336040

    #将本文件放置于natapp同级目录 程序将读取 [default] 段
    #在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置
    #命令行参数 -config= 可以指定任意config.ini文件
    [default]
    authtoken=改为自己申请隧道对应的authtoken      #对应一条隧道的authtoken
    
  4. 启动netapp程序

    image-20231204105456254

  5. 修改Nacos中支付系统配置文件中异步回调地址改为最新域名

    image-20231204105544046

  6. 启动支付系统,拉取最新配置

  7. 创建购买订单或充值记录,产生微信交易

1.4.2 支付服务集成Seata

  1. tingshu_payment数据库中新增undo_log日志表

    CREATE TABLE `undo_log` (
     `id` bigint NOT NULL AUTO_INCREMENT,
     `branch_id` bigint NOT NULL,
     `xid` varchar(100) NOT NULL,
     `context` varchar(128) NOT NULL,
     `rollback_info` longblob NOT NULL,
     `log_status` int NOT NULL,
     `log_created` datetime NOT NULL,
     `log_modified` datetime NOT NULL,
     `ext` varchar(100) DEFAULT NULL,
     PRIMARY KEY (`id`),
     UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3;
    
  2. service-payment模块pom.xml中增加Seata启动依赖

    <!--seata-->
    <dependency>
       <groupId>com.alibaba.cloud</groupId>
       <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
       <!-- 默认seata客户端版本比较低,排除后重新引入指定版本-->
       <exclusions>
           <exclusion>
               <groupId>io.seata</groupId>
               <artifactId>seata-spring-boot-starter</artifactId>
           </exclusion>
       </exclusions>
    </dependency>
    <dependency>
       <groupId>io.seata</groupId>
       <artifactId>seata-spring-boot-starter</artifactId>
    </dependency>
    
  3. 订单微服务service-order模块新增Seata配置

    seata:
     enabled: true
     tx-service-group: ${spring.application.name}-group # 事务组名称
     service:
       vgroup-mapping:
         #指定事务分组至集群映射关系,集群名default需要与seata-server注册到Nacos的cluster保持一致
         service-payment-group: default
     registry:
       type: nacos # 使用nacos作为注册中心
       nacos:
         server-addr: 192.168.200.6:8848 # nacos服务地址
         group: DEFAULT_GROUP # 默认服务分组
         namespace: "" # 默认命名空间
         cluster: default # 默认TC集群名称
       
    

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

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

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> paySuccessNotify(HttpServletRequest request) {
    Map<String, String> mapResult = wxPayService.paySuccessNotify(request);
    return mapResult;
}

WxPayService接口

/**
 * 用户付款成功后,处理微信支付异步回调
 * @param request
 * @return
 */
Map<String, String> paySuccessNotify(HttpServletRequest request);

WxPayServiceImpl实现类

@Autowired
private RedisTemplate redisTemplate;

/**
 * 用户通过微信支付支付成功后,处理微信支付异步回调
 * (微信通知商户用户支付结果)
 *
 * @param request
 * @return {code:"SUCCESS",message:"处理成功"}
 */
@Override
@GlobalTransactional(rollbackFor = Exception.class) //事务发起方开启全局事务
public Map<String, String> wxPaySuccessNotify(HttpServletRequest request) {
    //1.签名验证-避免出现“假通知”或者数据在网格传输中被恶意篡改
    //1.1 通过请求头获取构建RequestParam相关参数值
    String wechatPaySerial = request.getHeader("Wechatpay-Serial");  //签名
    String nonce = request.getHeader("Wechatpay-Nonce");  //签名中的随机数
    String timestamp = request.getHeader("Wechatpay-Timestamp"); //时间戳
    String signature = request.getHeader("Wechatpay-Signature"); //签名类型
    //通过工具类将所有请求体参数获取到
    String requestBody = PayUtil.readData(request);
    RequestParam requestParam = new RequestParam.Builder()
            .serialNumber(wechatPaySerial)
            .nonce(nonce)
            .signature(signature)
            .timestamp(timestamp)
            .body(requestBody)
            .build();
    //1.2 初始化NotificationParser通知验签对象
    NotificationParser parser = new NotificationParser(rsaAutoCertificateConfig);

    //1.3 基于NotificationParser完成验签、解密并转换成 Transaction(交易对象)
    Transaction transaction = parser.parse(requestParam, Transaction.class);
    log.info("微信验签解密得到交易对象:{}", transaction);
    //2.业务验证
    //2.1 利于Redis set ex nx 幂等性处理 正常业务只处理一次
    String orderNo = transaction.getOutTradeNo();//商户侧订单编号
    String key = "order:notify:" + orderNo;
    Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "", 25, TimeUnit.HOURS);
    if (flag) {
        try {
            //2.2 验证订单编号对应金额跟支付成功金额是否一致 TODO:实际产生支付金额1分,本地交易记录中存放实际应付金额
            Integer payerTotal = transaction.getAmount().getPayerTotal();
            if (payerTotal.intValue() == 1) {
                //3.处理本地交易记录-变更本地交易记录状态
                paymentInfoService.updatePaymentStatus(transaction, SystemConstant.ORDER_PAY_WAY_WEIXIN);
                //4.构建结果响应微信支付端
                Map<String, String> map = new HashMap<>();
                map.put("code", "SUCCESS");
                map.put("message", "操作成功!");
                return map;
            }
        } catch (Exception e) {
            log.error("[支付服务]处理异步回调异常", e);
            redisTemplate.delete(key);
            throw new RuntimeException(e);
        }
    }
    return null;
}

1.4.3.1 更新本地交易记录

PaymentInfoService

/**
 * 支付成功后,处理本地交易记录以及订单业务处理、充值业务处理
 * @param transaction 微信交易对象
 * @param payWay 支付方式
 */
void updatePaymentStatus(Transaction transaction, String payWay);

PaymentInfoServiceImpl

/**
 * 支付成功后,处理本地交易记录以及订单业务处理、充值业务处理
 *
 * @param transaction 微信交易对象
 * @param payWay      支付方式
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void updatePaymentStatus(Transaction transaction, String payWay) {
    //1.根据订单编号+付款方式查询本地交易记录
    String orderNo = transaction.getOutTradeNo();
    LambdaQueryWrapper<PaymentInfo> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(PaymentInfo::getOrderNo, orderNo);
    queryWrapper.eq(PaymentInfo::getPayWay, payWay);
    PaymentInfo paymentInfo = this.getOne(queryWrapper);
    if (paymentInfo != null && SystemConstant.PAYMENT_STATUS_UNPAID.equals(paymentInfo.getPaymentStatus())) {
        //2.更新本地交易记录。状态:已支付
        paymentInfo.setPaymentStatus(SystemConstant.PAYMENT_STATUS_PAID);
        //微信/支付宝端交易订单号
        paymentInfo.setOutTradeNo(transaction.getTransactionId());
        paymentInfo.setCallbackContent(transaction.toString());
        paymentInfo.setCallbackTime(new Date());
        this.updateById(paymentInfo);

        //3.处理不同支付类型:1301-订单 1302-充值
        String paymentType = paymentInfo.getPaymentType();
        if (SystemConstant.PAYMENT_TYPE_ORDER.equals(paymentType)) {
            //3.1 TODO 处理支付类型为订单:远程调用订单服务完成
            Result result = orderFeignClient.orderPaySuccess(orderNo);
            if (!ResultCodeEnum.SUCCESS.getCode().equals(result.getCode())) {
                log.error("[支付服务]远程调用订单处理订单支付成功异常", result.getMessage());
                throw new GuiguException(400, "变更订单状态异常!");
            }
        } else if (SystemConstant.PAYMENT_TYPE_RECHARGE.equals(paymentType)) {
            //3.2 TODO 处理支付类型为充值:远程调用账户服务完成
			Result result = accountFeignClient.rechargePaySuccess(orderNo);
            if (200 != result.getCode()) {
                throw new GuiguException(500, "远程修改余额异常:" + orderNo);
            }
        }

    }
}

1.4.3.2 订单服务-更新订单状态

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

service-order模块OrderInfoApiController处理订单支付成功

/**
 * 在线付款成功后-更新订单支付状态(新增购买记录)
 * @param orderNo
 * @return
 */
@Operation(summary = "在线付款成功后-更新订单支付状态(新增购买记录)")
@GetMapping("/orderInfo/orderPaySuccess/{orderNo}")
public Result orderPaySuccess(@PathVariable String orderNo){
    orderInfoService.orderPaySuccess(orderNo);
    return Result.ok();
}

OrderInfoService

/**
 * 在线付款成功后-更新订单支付状态(新增购买记录)
 * @param orderNo
 * @return
 */
void orderPaySuccess(String orderNo);

/**
 * 查询当前用户指定订单信息
 * @param orderNo 订单编号
 * @return 订单信息包含:商品列表,优惠列表
 */
OrderInfo getOrderInfoByOrderNo(String orderNo);

OrderInfoServiceImpl

/**
 * 在线付款成功后-更新订单支付状态(新增购买记录)
 *
 * @param orderNo
 * @return
 */
@Override
public void orderPaySuccess(String orderNo) {
    //1.更新订单状态
    //1.1 根据订单编号查询订单 判断状态
    OrderInfo orderInfo = this.getOrderInfoByOrderNo(orderNo);
    if (orderInfo != null) {
        //1.2 更新订单状态:已支付
        orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID);
        orderInfoMapper.updateById(orderInfo);
        //2.远程调用用户服务进行虚拟物品发货
        //2.1 构建购买记录VO对象
        UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo();
        userPaidRecordVo.setOrderNo(orderNo);
        userPaidRecordVo.setUserId(orderInfo.getUserId());
        userPaidRecordVo.setItemType(orderInfo.getItemType());
        List<Long> itemIdList = orderInfo.getOrderDetailList()
                .stream()
                .map(OrderDetail::getItemId)
                .collect(Collectors.toList());
        userPaidRecordVo.setItemIdList(itemIdList);

        //2.2 远程调用结果判断业务状态必须200 反之:抛出异常,全局事务回滚
        Result result = userFeignClient.savePaidRecord(userPaidRecordVo);
        if (!ResultCodeEnum.SUCCESS.getCode().equals(result.getCode())) {
            log.error("[订单服务]远程调用用户服务虚拟物品发货异常", result.getMessage());
            throw new GuiguException(400, "购买记录新增异常!");
        }
    }

}

//单独根据订单编号查询订单信息(无用户ID)
@Override
public OrderInfo getOrderInfoByOrderNo(String orderNo) {
    //1.根据订单编号+用户ID查询订单信息
    LambdaQueryWrapper<OrderInfo> orderInfoLambdaQueryWrapper = new LambdaQueryWrapper<>();
    orderInfoLambdaQueryWrapper.eq(OrderInfo::getOrderNo, orderNo);
    OrderInfo orderInfo = orderInfoMapper.selectOne(orderInfoLambdaQueryWrapper);

    if (orderInfo != null) {
        //2.根据订单ID查询订单明细列表
        LambdaQueryWrapper<OrderDetail> orderDetailLambdaQueryWrapper = new LambdaQueryWrapper<>();
        orderDetailLambdaQueryWrapper.eq(OrderDetail::getOrderId, orderInfo.getId());
        List<OrderDetail> orderDetailList = orderDetailMapper.selectList(orderDetailLambdaQueryWrapper);
        orderInfo.setOrderDetailList(orderDetailList);


        //3.根据订单ID查询订单优惠列表
        LambdaQueryWrapper<OrderDerate> orderDerateLambdaQueryWrapper = new LambdaQueryWrapper<>();
        orderDerateLambdaQueryWrapper.eq(OrderDerate::getOrderId, orderInfo.getId());
        List<OrderDerate> orderDerateList = orderDerateMapper.selectList(orderDerateLambdaQueryWrapper);
        orderInfo.setOrderDerateList(orderDerateList);
    }
    return orderInfo;
}

service-order-client模块OrderFeignClient中新增Feign远程调用方法

/**
 * 在线付款成功后-更新订单支付状态(新增购买记录)
 * @param orderNo
 * @return
 */
@GetMapping("/orderInfo/orderPaySuccess/{orderNo}")
public Result orderPaySuccess(@PathVariable String orderNo);

OrderDegradeFeignClient服务降级类

@Override
public Result orderPaySuccess(String orderNo) {
    log.error("[订单服务]提供远程调用orderPaySuccess服务降级");
    return null;
}

1.4.3.3 账户服务-更新账户信息

见第2小节。

2、充值业务

2.0 保存充值记录

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

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, AuthContextHolder.getUserId());
    return Result.ok(map);
}

RechargeInfoService接口:

/**
 * 保存充值余额记录返回充值订单编号
 * @param rechargeInfoVo
 * @param userId
 * @return
 */
Map<String, String> submitRecharge(RechargeInfoVo rechargeInfoVo, Long userId);

实现类:

/**
 * 保存充值余额记录返回充值订单编号
 *
 * @param rechargeInfoVo
 * @param userId
 * @return
 */
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, String> submitRecharge(RechargeInfoVo rechargeInfoVo, Long userId) {
    //1.构建充值记录对象
    RechargeInfo rechargeInfo = new RechargeInfo();
    rechargeInfo.setUserId(userId);
    //生成全局唯一充值订单编号
    String orderNo = "CZ" + DateUtil.today().replaceAll("-", "") + IdUtil.getSnowflakeNextId();
    rechargeInfo.setOrderNo(orderNo);
    rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_UNPAID);
    rechargeInfo.setRechargeAmount(rechargeInfoVo.getAmount());
    rechargeInfo.setPayWay(rechargeInfoVo.getPayWay());
    this.save(rechargeInfo);
    //2.返回充值记录充值订单编号
    Map<String, String> map = new HashMap<>();
    map.put("orderNo", orderNo);
    return map;
}

2.1 充值成功业务处理

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

  • service-account 模块添加处理充值成功的充值订单业务Restful接口即可

2.1.1 修改充值状态

YAPI接口文档地址:http://192.168.200.6:3000/project/11/interface/api/154

在这个UserAccountApiController类中添加数据

/**
 * 在线付款成功后-更新充值状态&账户余额及新增账户变动日志
 *
 * @param orderNo
 * @return
 */
@Operation(summary = "")
@GetMapping("/rechargeInfo/rechargePaySuccess/{orderNo}")
public Result rechargePaySuccess(@PathVariable String orderNo) {
    rechargeInfoService.rechargePaySuccess(orderNo);
    return Result.ok();
}

UserAccountService接口

/**
 * 在线付款成功后-更新充值状态&账户余额及新增账户变动日志
 * @param orderNo
 */
void rechargePaySuccess(String orderNo);

RechargeInfoServiceImpl实现

@Autowired
private UserAccountService userAccountService;

/**
 * 在线付款成功后-更新充值状态&账户余额及新增账户变动日志
 *
 * @param orderNo
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void rechargePaySuccess(String orderNo) {
    //1.根据订单编号查询充值记录
    LambdaQueryWrapper<RechargeInfo> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(RechargeInfo::getOrderNo, orderNo);
    RechargeInfo rechargeInfo = this.getOne(queryWrapper);
    if (rechargeInfo != null) {
        //2.更新充值记录状态:支付成功
        rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_PAID);
        this.updateById(rechargeInfo);

        //3.账户余额入账
        UserAccountMapper userAccountMapper = (UserAccountMapper) userAccountService.getBaseMapper();
        int count = userAccountMapper.add(rechargeInfo.getUserId(), rechargeInfo.getRechargeAmount());
        if (count == 0) {
            throw new GuiguException(500, "充值失败");
        }
        //4.新增账户变动日志
        userAccountService.saveUserAccountDetail(
                rechargeInfo.getUserId(),
                "充值:"+rechargeInfo.getRechargeAmount(),
                SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT,
                rechargeInfo.getRechargeAmount(),
                rechargeInfo.getOrderNo()
        );
    }
}

2.1.2 账户充值

UserAccountMapper

/**
 * 账户充值
 * @param userId
 * @param rechargeAmount
 * @return
 */
int add(@Param("userId") Long userId, @Param("amount") BigDecimal amount)

UserAccountMapper.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.1.3 提供Feign接口

service-account-client模块中提供远程调用Feign接口

AccountFeignClient

/**
 * 在线付款成功后-更新充值状态&账户余额及新增账户变动日志
 *
 * @param orderNo
 * @return
 */
@GetMapping("/rechargeInfo/rechargePaySuccess/{orderNo}")
public Result rechargePaySuccess(@PathVariable String orderNo);

AccountDegradeFeignClient服务降级类

@Override
public Result rechargePaySuccess(String orderNo) {
    log.error("[账户服务]提供远程调用方法rechargePaySuccess服务降级");
    return null;
}

2.2 充值记录

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

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

/**
 * 分页查询当前用户充值记录
 *
 * @param page
 * @param limit
 * @return
 */
@GuiGuLogin
@Operation(summary = "分页查询当前用户充值记录")
@GetMapping("/userAccount/findUserRechargePage/{page}/{limit}")
public Result<Page<UserAccountDetail>> getUserRechargePage(@PathVariable int page, @PathVariable int limit) {
    Page<UserAccountDetail> pageInfo = new Page<>(page, limit);
    userAccountService.getUserAccountDetailPage(pageInfo, SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT);
    return Result.ok(pageInfo);
}

UserAccountService接口:

/**
 * 分页查询当前用户充值、消费记录
 *
 * @param pageInfo 分页对象
 * @param tradeType 交易类型:1201-充值 1204-消费
 * @return
 */
void getUserAccountDetailPage(Page<UserAccountDetail> pageInfo, String tradeType);

UserAccountServiceImpl实现类:

/**
 * 分页查询当前用户充值、消费记录
 *
 * @param pageInfo 分页对象
 * @param tradeType 交易类型:1201-充值 1204-消费
 * @return
 */
@Override
public void getUserAccountDetailPage(Page<UserAccountDetail> pageInfo, String tradeType) {
    //1.获取当前登录用户ID
    Long userId = AuthContextHolder.getUserId();
    //2.调用持久层获取充值记录
    pageInfo = userAccountMapper.getUserAccountDetailPage(pageInfo, userId, tradeType);
}
package com.atguigu.tingshu.account.mapper;

import com.atguigu.tingshu.model.account.UserAccountDetail;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface UserAccountDetailMapper extends BaseMapper<UserAccountDetail> {

   /**
 * 分页查询当前用户充值记录
 * @param pageInfo
 * @param userId
 * @return
 */
Page<UserAccountDetail> getUserAccountDetailPage(Page<UserAccountDetail> pageInfo, @Param("userId") Long userId, @Param("tradeType") String tradeType);
    
}

xml 配置文件

<!--分页查询当前用户充值/消费记录-->
<select id="getUserAccountDetailPage" resultType="com.atguigu.tingshu.model.account.UserAccountDetail">
	select *
	from user_account_detail
	where user_id = #{userId} and trade_type = #{tradeType} 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 微服务中添加

/**
 * 分页查询当前用户消费记录
 *
 * @param page
 * @param limit
 * @return
 */
@GuiGuLogin
@Operation(summary = "分页查询当前用户消费记录")
@GetMapping("/userAccount/findUserConsumePage/{page}/{limit}")
public Result<Page<UserAccountDetail>> getUserConsumePage(@PathVariable int page, @PathVariable int limit) {
    Page<UserAccountDetail> pageInfo = new Page<>(page, limit);
    userAccountService.getUserAccountDetailPage(pageInfo, SystemConstant.ACCOUNT_TRADE_TYPE_MINUS);
    return Result.ok(pageInfo);
}

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

充值记录: