Java对接微信H5支付-V3版本接口

系列文章:

1、Java对接微信JS支付-V3版本接口
2、Java对接微信H5支付-V3版本接口
3、Java对接微信Native(扫码)支付-V3版本接口

前言: 近期因为公司业务需求,需要将所负责的项目对接微信H5支付,特来记录一下整个对接过程和遇到的一些坑

1. H5支付最好的使用场景是移动端浏览器
2. ios和Android APP内嵌H5页面都需要特殊的操作进行适配(下面会讲到)
3. 如果大家有在微信App内部使用的场景,一定要使用别的支付方式,因为微信App不支持使用H5支付
4. 使用H5支付的时候,需要在商户后台添加项目对应的域名,审核周期需要七天,如果确认要使用的话,一定要第一时间在商户后台进行支付域名的申请,可以直接申请一级域名,二级域名也可以使用

对接流程图:
在这里插入图片描述

-----------------------------------------------------对接开始-----------------------------------------------------

1. 查阅微信对接文档

1、H5支付产品介绍
2、H5下单API
3、github代码参考

2. 数据准备

微信官网有详细的文档介绍,你需要准备那些东西
1、H5支付接入前准备

3. Java代码

1. 引入微信官网依赖

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.12</version>
</dependency>

2. 编写WeChatConfig,将微信公共配置进行配置化

/**
 * @desc: 微信config
 * @author: shy
 * @date: 2024/4/9 10:06
 */
@Configuration
@Getter
public class WeChatConfig {

    /**
     * 商户号
     */
    @Value("${wechat.pay.merchantId}")
    public String merchantId;
    /**
     * 商户API私钥路径
     */
    @Value("${wechat.pay.privateKeyPath}")
    public String privateKeyPath;
    /**
     * 商户证书序列号
     */
    @Value("${wechat.pay.merchantSerialNumber}")
    public String merchantSerialNumber;
    /**
     * 商户APIV3密钥
     */
    @Value("${wechat.pay.apiV3Key}")
    public String apiV3Key;
    /**
     * AppId
     */
    @Value("${wechat.pay.appId}")
    public String appId;

    private Config config;

    @PostConstruct
    public void initConfig() {
        // 使用自动更新平台证书的RSA配置
        // 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
        config = new RSAAutoCertificateConfig.Builder()
                .merchantId(merchantId)
                .privateKeyFromPath(privateKeyPath)
                .merchantSerialNumber(merchantSerialNumber)
                .apiV3Key(apiV3Key)
                .build();
    }

    @Primary
    @Bean()
    public H5Service h5Service() {
        return new H5Service.Builder()
                .config(config)
                .build();
    }

    @Primary
    @Bean
    public NotificationParser notificationParser() {
        return new NotificationParser((NotificationConfig) config);
    }
}

3. 编写预下单接口

controller
@Autowired
WechatService wechatService;

@RequestMapping("/v3/h5Pay")
public Result<String> h5Pay(Integer memberFeeType, HttpServletRequest request) {
    MemberFeeTypeEnum memberFeeTypeEnum = MemberFeeTypeEnum.getMemberFeeTypeEnumByType(memberFeeType);
    if (Objects.isNull(memberFeeTypeEnum)) {
        return Result.fail(ResultEnum.PARAM_ERROR);
    }
    // 返回h5支付的url
    String h5Url = wechatService.h5Pay(memberFeeType, request, memberFeeTypeEnum);
    if (StringUtils.isBlank(h5Url)) {
        return Result.fail("支付失败");
    }
    return Result.success(h5Url);
}
service
@Resource
private WechatPayOrderMapper wechatPayOrderMapper;
@Resource
private H5Service h5Service;

public String h5Pay(Integer memberFeeType, HttpServletRequest request, MemberFeeTypeEnum memberFeeTypeEnum) {
    // request.setXxx(val)设置所需参数,具体参数可见Request定义
    PrepayRequest prepayRequest = new PrepayRequest();
    Amount amount = new Amount();
    amount.setTotal(memberFeeTypeEnum.getPrice());
    prepayRequest.setAmount(amount);
    prepayRequest.setAppid(appId);
    prepayRequest.setMchid(merchantId);
    prepayRequest.setDescription(memberFeeTypeEnum.getDesc());
    prepayRequest.setNotifyUrl(notifyUrl);
    String tradeNo = WeChatUtil.generateTradeNumber();
    prepayRequest.setOutTradeNo(tradeNo);
    SceneInfo sceneInfo = new SceneInfo();
    sceneInfo.setPayerClientIp(WebUtil.getIP(request));
    H5Info h5Info = new H5Info();
    h5Info.setType(UserAgent.parseUserAgentString(request.getHeader("User-Agent")).getOperatingSystem().getName());
    sceneInfo.setH5Info(h5Info);
    prepayRequest.setSceneInfo(sceneInfo);
    // 调用下单方法,得到应答
    PrepayResponse response;
    try {
        response = h5Service.prepay(prepayRequest);
        //预支付成功,创建预支付订单
        WechatPayOrder wechatPayOrder = new WechatPayOrder();
        //获取当前用户
        String mobile = (String) request.getSession().getAttribute(Constants.MOBILE);
        User user = redisTemplateUser.opsForValue().get(redis_user_prefix + mobile);
        if (Objects.isNull(user)) {
            log.error("用户信息不存在");
            return null;
        }
        wechatPayOrder.setUserId(user.getId());
        wechatPayOrder.setTradeNo(tradeNo);
        wechatPayOrder.setMemberFeeType(memberFeeType);
        wechatPayOrder.setAmount(memberFeeTypeEnum.getPrice());
        wechatPayOrderMapper.addOrder(CoverUtil.coverMap(wechatPayOrder));
        return response.getH5Url();
    } catch (HttpException e) { // 发送HTTP请求失败
        log.error("发送HTTP请求失败: {}", e.getHttpRequest());
    } catch (ServiceException e) { // 服务返回状态小于200或大于等于300,例如500
        log.error("服务返回状态异常: {}", e.getResponseBody());
    } catch (MalformedMessageException e) { // 服务返回成功,返回体类型不合法,或者解析返回体失败
        log.error("返回体类型不合法: {}", e.getMessage());
    } catch (Exception e) {
        log.error("预下单异常: {}", e.getMessage());
    }
    return null;
}

4. 编写回调接口

controller
@RequestMapping("/v3/payNotify")
public WeChatPayResult payNotify(HttpServletRequest request) {
    try {
        return wechatService.payNotify(request);
    } catch (Exception e) {
        log.error("微信回调异常: ", e);
        return new WeChatPayResult("FAIL", "FAIL");
    }
}
service
@Resource
private NotificationParser notificationParser;
@Resource
private WechatPayOrderMapper wechatPayOrderMapper;

@Transactional
public WeChatPayResult payNotify(HttpServletRequest request) throws Exception{
    log.info("微信回调开始-------------------------");
    Transaction transaction;
    try {
        transaction = notificationParser.parse(WeChatUtil.handleNodifyRequestParam(request), Transaction.class);
        if (transaction.getTradeState() == Transaction.TradeStateEnum.SUCCESS) {
            WechatPayOrder wechatPayOrder = wechatPayOrderMapper.getOrderByTradeNo(transaction.getOutTradeNo());
            if (Objects.isNull(wechatPayOrder)) {
                log.error("订单不存在, 订单号: {}", transaction.getOutTradeNo());
                return new WeChatPayResult("SUCCESS", "ORDER_DOES_NOT_EXIST");
            }
            //校验订单状态:若订单已支付则直接返回成功
            if (Objects.equals(wechatPayOrder.getTradeStatus(), WeChatPayTradeStatusEnum.已支付.getType())) {
                log.error("订单已支付, 订单号: {}", transaction.getOutTradeNo());
                return new WeChatPayResult("SUCCESS", "ORDER_PAID");
            }
            //支付成功-修改订单状态
            wechatPayOrderMapper.updateTradeStatus(transaction.getOutTradeNo(), transaction.getTransactionId(), wechatPayOrder.getVersion());
            //支付成功-添加会员
            MemberFeeTypeEnum memberFeeTypeEnum = MemberFeeTypeEnum.getMemberFeeTypeEnumByType(wechatPayOrder.getMemberFeeType());
            userAwardService.addAward(wechatPayOrder.getUserId(), memberFeeTypeEnum.getMemberDays());
        }
    } catch (ValidationException e) {
        // 签名验证失败,返回 401 UNAUTHORIZED 状态码
        log.error("签名验证失败: ", e);
        return new WeChatPayResult("FAIL", "UNAUTHORIZED");
    }
    log.info("微信回调结束, 微信回调报文: {}", transaction);
    return new WeChatPayResult("SUCCESS", "SUCCESS");
}

5. 编写微信工具类

微信的依赖中有很多好用的工具类,但是在他们的文档中都没有体现,需要耐心地找一下
在这里插入图片描述

WeChatUtil
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.util.Random;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;

import com.wechat.pay.java.core.cipher.RSASigner;
import com.wechat.pay.java.core.cipher.SignatureResult;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.core.util.NonceUtil;
import com.wechat.pay.java.core.util.PemUtil;

/**
 * @desc: 微信工具类
 * @author: shy
 * @date: 2024/4/8 16:10
 */
public class WeChatUtil {

    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private static final Random RANDOM = new SecureRandom();

    /**
     * 生成订单号
     *
     * @param
     * @return String
     * @author shy
     * @date 2024/4/8 16:15
     */
    public static String generateTradeNumber() {
        // 定义订单号前缀
        String prefix = "shy";
        // 当前年月日
        String currentTimeStr = DateUtil.getCurrent("yyyyMMddHHmmss");
        // 获取当前时间戳
        long timestamp = System.currentTimeMillis();
        // 构造订单号
        return prefix + currentTimeStr + timestamp;
    }

    /**
     * 获取随机字符串 Nonce Str
     *
     * @param
     * @return String
     * @author shy
     * @date 2024/4/16 17:07
     */
    public static String generateNonceStr() {
        return NonceUtil.createNonce(32);
    }

    /**
     * 获取当前时间戳,单位秒
     * @param
     * @return long
     * @author shy
     * @date 2024/4/16 17:10
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis() / 1000;
    }

    public static String getSign(String signatureStr, String privateKeyPath, String merchantSerialNumber) {
        PrivateKey privateKey = PemUtil.loadPrivateKeyFromPath(privateKeyPath);
        RSASigner rsaSigner = new RSASigner(merchantSerialNumber, privateKey);
        SignatureResult signatureResult = rsaSigner.sign(signatureStr);
        return signatureResult.getSign();
    }


    /**
     * 构造 RequestParam
     *
     * @param request
     * @return RequestParam
     * @author shy
     * @date 2024/4/9 11:16
     */
    public static RequestParam handleNodifyRequestParam(HttpServletRequest request) throws IOException {
        // 请求头Wechatpay-Signature
        String signature = request.getHeader("Wechatpay-Signature");
        // 请求头Wechatpay-nonce
        String nonce = request.getHeader("Wechatpay-Nonce");
        // 请求头Wechatpay-Timestamp
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        // 微信支付证书序列号
        String serial = request.getHeader("Wechatpay-Serial");
        // 签名方式
        String signType = request.getHeader("Wechatpay-Signature-Type");
        // 构造 RequestParam
        return new RequestParam.Builder().serialNumber(serial).nonce(nonce).signature(signature).timestamp(timestamp).signType(signType).body(getRequestBody(request)).build();

    }

    public static String getRequestBody(HttpServletRequest request) throws IOException {
        ServletInputStream stream;
        BufferedReader reader = null;
        StringBuilder sb = new StringBuilder();
        try {
            stream = request.getInputStream();
            // 获取响应
            reader = new BufferedReader(new InputStreamReader(stream));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            throw new IOException("读取返回支付接口数据流出现异常!");
        } finally {
            if (reader != null) {
                reader.close();
            }
        }
        return sb.toString();
    }
}

6. 测试接口

测试需要使用互联网可以连通的域名,所以我是在生产环境新创建了一个二级域名做的测试,并且回调地址也必须是你申请审核通过的支付域名才可以。

7.ios和Android App内嵌H5页面解决方案

不建议在APP中使用H5支付,最好可以使用APP支付(微信官方说的)
微信官网解决方案:
在这里插入图片描述
我们根据自身情况使用的解决方案(移动端开发的同事写的,我不是很懂)

1、ios(处理无法调起微信客户端)

在这里插入图片描述
在这里插入图片描述

2.Android(Referer原因)

在这里插入图片描述
在这里插入图片描述

8. 总结

可能是我阅读文档的方式不太对,微信支付的官方开发文档写的真是一言难尽,看了好长时间才大致看明白。所以写下这篇文章,愿后面开发的兄弟们对接起来都可以开开心心,顺顺利利!一次成功!
在这里插入图片描述

  • 31
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值