结合IJPAY聚合支付实现微信小程序支付

一、参考文章

java微信小程序调用微信支付apiv3接口(最简洁代码)_微信小程序调用支付接口-优快云博客

新人小白第一次做的支付功能,基于大佬的文档做的开发,涉及一些个人在处理过程中遇到问题。

二、开发前准备

@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "wx")
public class WechatConfigProperties {

    private String appId;

    /*private String appSecret;*/

    //# 证书序列号
    private String mchSerialNo;

    //#平台证书地址,平台证书需要手动下载或者使用v3接口进行下载
    private String platformCertPath;

    //客户证书地址,也就是apiclient_cert.pem的路径
    private String apiClientCertPath;

    //#证书密钥地址,也就是apiclient_key.pem的路径
    private String apiClientKeyPath;

    //商户号
    private String mchId;

    //apiv3秘钥w
    private String apiKey;

    //微信服务器地址
    private String domain;

    //回调,自己的回调地址,暂时内网穿透
    private String notifyUrl;
}

     上文涉及到的几个参数,可以在微信平台申请,也可以用接口访问下载。相关文件在申请后可以配置到服务器上,笔者是将文件上传到服务器固定存放静态文件的文件夹下。在本地打包前可存放到resources根目录下。

三、具体代码

   1.下单请求参数

   这里的请求是按个人需求定制化处理的,因为笔者这里涉及一些后续逻辑处理,所以传递这三个参数。

@Data
@ApiModel("微信支付请求参数")
public class IWxPayParamVO {

    //操作人账号id
    private Long accountId;

    //下单金额
    private Integer amount;

    //下单类型
    private Integer payType;

}
  2.controller层
import com.dltech.edu.account.module.wxPay.domain.vo.IWxPayParamVO;
import com.dltech.edu.account.module.wxPay.service.impl.WeChatPayServiceImpl;
import com.eop.common.ssm.domain.vo.response.BaseResponse;
import com.eop.common.ssm.web.controller.BaseController;


import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@Api(tags = "微信支付接口")
@RestController
@RequestMapping("/payment")
public class WeChatPayController extends BaseController {

    @Autowired
    private WeChatPayServiceImpl wxPayNewService;

    /**
     * 微信统一下单接口
     *
     * @param iWxPayParamVO 业务需要的参数
     * @return
     */
    @ApiOperation("微信统一下单接口")
    @PostMapping("/doUnifiedOrder")
    public BaseResponse<Map<String, String>> doUnifiedOrder(@RequestBody IWxPayParamVO iWxPayParamVO) throws Exception {
        Map<String, String> stringStringMap = wxPayNewService.doUnifiedOrder(iWxPayParamVO);
        return success(stringStringMap);
    }

    /**
     * 微信支付的回调接收接口
     *
     * @param request
     * @param response
     */
    @ApiOperation("微信支付的回调接收接口")
    @RequestMapping(value = "/payNotify", method = {org.springframework.web.bind.annotation.RequestMethod.POST, org.springframework.web.bind.annotation.RequestMethod.GET})
    public void callBack(HttpServletRequest request, HttpServletResponse response) {
        wxPayNewService.callBack(request, response);
    }

    /**
     * 生成平台证书
     */
    @ApiOperation("生成v3证书")
    @RequestMapping("/createPlatformCert")
    @ResponseBody
    public String createPlatformCert() throws IOException {
        return wxPayNewService.createPlatformCert();
    }

    /**
     * 通过订单号查询支付情况
     *
     * @param outTradeNo 订单号
     * @return String
     */
    @ApiOperation("通过订单号查询支付情况")
    @RequestMapping("/query")
    @ResponseBody
    public String query(@RequestParam String outTradeNo) {
        return wxPayNewService.query(outTradeNo);
    }


    /**
     * 微信退款
     * outTradeNo: 原支付交易对应的商户订单号
     */
    @ApiOperation("申请退款接口")
    @GetMapping("/payRefund")
    public String payRefund( @RequestParam String outTradeNo) {
        return wxPayNewService.payRefund(outTradeNo);
    }
}

  这里的退款接口是可以传递transactionId 微信是支持两个订单号申请退款的。

  3.serviceImpl层
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dltech.edu.account.config.WechatConfigProperties;
import com.dltech.edu.account.module.ac.domain.entity.AcAccountWx;
import com.dltech.edu.account.module.ac.service.impl.AcAccountWxServiceImpl;
import com.dltech.edu.account.module.wxPay.domain.entity.WeChatPay;
import com.dltech.edu.account.module.wxPay.domain.vo.IWxPayParamVO;

import com.dltech.edu.account.module.wxPay.mapper.WeChatPayMapper;
import com.ijpay.core.IJPayHttpResponse;
import com.ijpay.core.enums.AuthTypeEnum;
import com.ijpay.core.enums.RequestMethodEnum;
import com.ijpay.core.kit.AesUtil;
import com.ijpay.core.kit.HttpKit;
import com.ijpay.core.kit.PayKit;
import com.ijpay.core.kit.WxPayKit;
import com.ijpay.core.utils.DateTimeZoneUtil;
import com.ijpay.wxpay.WxPayApi;
import com.ijpay.wxpay.enums.WxDomainEnum;
import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
import com.ijpay.wxpay.enums.v3.CertAlgorithmTypeEnum;
import com.ijpay.wxpay.model.v3.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.*;

import static com.eop.common.cache.CacheProvider.logger;

@Slf4j
@Service
public class WeChatPayServiceImpl extends ServiceImpl<WeChatPayMapper, WeChatPay> {

    @Autowired
    private AcAccountWxServiceImpl acAccountWxService;

    @Autowired
    private  WechatConfigProperties wechatConfigProperties;

    public WeChatPayServiceImpl(WechatConfigProperties wechatConfigProperties) {
        this.wechatConfigProperties = wechatConfigProperties;
    }


    /**
     * 微信统一下单接口
     *
     * @return Map
     * @throws Exception 异常
     */
    public Map<String, String> doUnifiedOrder(IWxPayParamVO iWxPayParamVO) throws Exception {
        
        log.info("当前账户id {}",iWxPayParamVO.getAccountId());
        AcAccountWx acAccountWx = acAccountWxService.selectByAccountId(iWxPayParamVO.getAccountId());
        //用户登录时候微信返回的openId(注意不是unionId)
        String openId = "";
        
        //先写死1, 1就是1分钱,100=1元
        String timeExpire = DateTimeZoneUtil.dateToTimeZone(System.currentTimeMillis() + 1000 * 60 * 3);
        UnifiedOrderModel unifiedOrderModel = new UnifiedOrderModel()
                .setAppid(wechatConfigProperties.getAppId())
                .setMchid(wechatConfigProperties.getMchId())
                .setDescription("支付说明")
                //这是随机数
                .setOut_trade_no(PayKit.generateStr())
                .setTime_expire(timeExpire)
                .setAttach("附加说明")
                //回调地址
                .setNotify_url(wechatConfigProperties.getNotifyUrl())
                .setAmount(new Amount().setTotal(iWxPayParamVO.getAmount()))
                .setPayer(new Payer().setOpenid(openId));
        log.info("统一下单参数 {}", JSONUtil.toJsonStr(unifiedOrderModel));

        IJPayHttpResponse response = WxPayApi.v3(
                RequestMethodEnum.POST,
                //中国境内的微信支付环境
                WxDomainEnum.CHINA.toString(),
                //微信支付API接口
                BasePayApiEnum.JS_API_PAY.toString(),
                wechatConfigProperties.getMchId(),
                //获取证书序列号
                getSerialNumber(),
                null,
                wechatConfigProperties.getApiClientKeyPath(),
                JSONUtil.toJsonStr(unifiedOrderModel)
        );
        log.info("统一下单响应 {}", response);
        Map<String, String> map = new HashMap<>(16);
        //这个是证书文件,先写死,后续调整成读取证书文件的服务器存放地址,根据证书序列号查询对应的证书来验证签名结果
        boolean verifySignatures = WxPayKit.verifySignature(response, wechatConfigProperties.getApiClientCertPath());
        log.info("verifySignature: {}", verifySignatures);
        if (response.getStatus() == HttpServletResponse.SC_OK) {
            // 根据证书序列号查询对应的证书来验证签名结果
            String body = response.getBody();
            JSONObject jsonObject = JSONUtil.parseObj(body);
            String prepayId = jsonObject.getStr("prepay_id");
            // 私钥
            map = WxPayKit.jsApiCreateSign(wechatConfigProperties.getAppId(), prepayId, wechatConfigProperties.getApiClientKeyPath());
            log.info("唤起支付参数:{}", map);

        }
        // todo 微信预支付的订单新入库,为了业务查询记录,可以做存入数据库操作
      
        return map;
    }

    /**
     * 微信支付回调接口
     *
     * @param request  请求
     * @param response 详情
     */

    public void callBack(HttpServletRequest request, HttpServletResponse response) {
        log.info("收到微信支付回调");
        Map<String, String> map = new HashMap<>(12);
        try {
            String timestamp = request.getHeader("Wechatpay-Timestamp");
            String nonce = request.getHeader("Wechatpay-Nonce");
            String serialNo = request.getHeader("Wechatpay-Serial");
            String signature = request.getHeader("Wechatpay-Signature");

            log.info("timestamp:{} nonce:{} serialNo:{} signature:{}", timestamp, nonce, serialNo, signature);
            String result = HttpKit.readData(request);
            log.info("支付通知密文 {}", result);
            // 根据证书序列号查询对应的证书来验证签名结果
            String platformCertPath = this.wechatConfigProperties.getPlatformCertPath();
            log.info("平台证书地址 {}", platformCertPath);
            //这个商户号对应的那个V3秘钥
            String mckKey = wechatConfigProperties.getApiKey();
            //需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
            String plainText = WxPayKit.verifyNotify(serialNo, result, signature, nonce, timestamp, mckKey, platformCertPath);
            log.info("支付通知明文 {}", plainText);
            //这个就是具体的业务情况
            savePayPlainText(plainText);
            //回复微信
            if (StrUtil.isNotEmpty(plainText)) {
                response.setStatus(200);
                map.put("code", "SUCCESS");
                map.put("message", "SUCCESS");
            } else {
                response.setStatus(500);
                map.put("code", "ERROR");
                map.put("message", "签名错误");
            }
            response.setHeader("Content-type", ContentType.JSON.toString());
            response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
            response.flushBuffer();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 查询账单
     *
     * @param outTradeNo
     * @return
     */

    public String query(String outTradeNo) {
        try {
            Map<String, String> params = new HashMap<>(16);
            params.put("mchid", wechatConfigProperties.getMchId());

            log.info("统一下单参数 {}", JSONUtil.toJsonStr(params));
            IJPayHttpResponse response = WxPayApi.v3(
                    RequestMethodEnum.GET,
                    WxDomainEnum.CHINA.toString(),
                    String.format(BasePayApiEnum.ORDER_QUERY_BY_OUT_TRADE_NO.toString(), outTradeNo),
                    wechatConfigProperties.getMchId(),
                    getSerialNumber(),
                    null,
                    wechatConfigProperties.getApiClientKeyPath(),
                    params
            );
            log.info("查询响应 {}", response);
            if (response.getStatus() == HttpServletResponse.SC_OK) {
                // 根据证书序列号查询对应的证书来验证签名结果
                boolean verifySignature = WxPayKit.verifySignature(response, wechatConfigProperties.getPlatformCertPath());
                log.info("verifySignature: {}", verifySignature);
                return response.getBody();
            }
            return JSONUtil.toJsonStr(response);
        } catch (Exception e) {
            log.error("系统异常", e);
            return e.getMessage();
        }
    }

    /**
     * 获取v3的证书
     *
     * @return String类型
     */

    public String createPlatformCert() {
        //这个商户号对应的那个V3秘钥
        String mckKey = wechatConfigProperties.getApiKey();
        // 获取平台证书列表
        try {
            IJPayHttpResponse response = WxPayApi.v3(
                    RequestMethodEnum.GET,
                    WxDomainEnum.CHINA.toString(),
                    CertAlgorithmTypeEnum.getCertSuffixUrl(CertAlgorithmTypeEnum.RSA.getCode()),
                    wechatConfigProperties.getMchId(),
                    getSerialNumber(),
                    null,
                    wechatConfigProperties.getApiClientKeyPath(),
                    "",
                    AuthTypeEnum.RSA.getCode()
            );
            String serialNumber = response.getHeader("Wechatpay-Serial");
            String body = response.getBody();
            int status = response.getStatus();
            log.info("serialNumber: {}", serialNumber);
            log.info("status: {}", status);
//            log.info("body: {}", body);
            if (status == HttpServletResponse.SC_OK) {
                JSONObject jsonObject = JSONUtil.parseObj(body);
                JSONArray dataArray = jsonObject.getJSONArray("data");
                // 默认认为只有一个平台证书
                JSONObject encryptObject = dataArray.getJSONObject(0);
                JSONObject encryptCertificate = encryptObject.getJSONObject("encrypt_certificate");
                String associatedData = encryptCertificate.getStr("associated_data");
                String cipherText = encryptCertificate.getStr("ciphertext");
                String nonce = encryptCertificate.getStr("nonce");
                String serialNo = encryptObject.getStr("serial_no");
                //生成一个证书文件,根据秘钥对应的证书
                final String platSerialNo = savePlatformCert(associatedData, mckKey, nonce, cipherText, wechatConfigProperties.getPlatformCertPath());
                log.info("平台证书序列号: {} serialNo: {}", platSerialNo, serialNo);
            }
            // 根据证书序列号查询对应的证书来验证签名结果
            boolean verifySignature = WxPayKit.verifySignature(response, wechatConfigProperties.getPlatformCertPath());
            if (verifySignature) {
                return body;
            } else {
                return "平台证书不正确";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "不能获取平台证书";
        }
    }

    private String savePlatformCert(String associatedData, String apiKey3, String nonce, String cipherText, String certPath) {
        try {
            AesUtil aesUtil = new AesUtil(apiKey3.getBytes(StandardCharsets.UTF_8));
            // 平台证书密文解密
            // encrypt_certificate 中的  associated_data nonce  ciphertext
            String publicKey = aesUtil.decryptToString(
                    associatedData.getBytes(StandardCharsets.UTF_8),
                    nonce.getBytes(StandardCharsets.UTF_8),
                    cipherText
            );
//            log.info("获取证书key:{},保存路径platformCert:{}", publicKey, certPath);
            log.info("保存路径platformCert:{}", certPath);
            //将生成的证书写入指定路径,文件名为:cert.pem
            FileOutputStream fos = new FileOutputStream(certPath);
            fos.write(publicKey.getBytes());
            fos.close();
            // 获取平台证书序列号
            X509Certificate certificate = PayKit.getCertificate(new ByteArrayInputStream(publicKey.getBytes()));
            return certificate.getSerialNumber().toString(16).toUpperCase();
        } catch (Exception e) {
            log.error("写入证书错误:{}", e);
            return e.getMessage();
        }
    }

    /**
     * 保存订单的支付通知明文
     *
     * @param plainText 纯文本
     */
    private void savePayPlainText(String plainText) {
        JSONObject jsonObject = JSONUtil.parseObj(plainText);
        //这个就是发起订单时的那个订单号
        String outTradeNo = jsonObject.getStr("out_trade_no");
        //todo 把微信支付回调的明文消息存进数据库,方便后续校验查看
       
        // 保存到数据库
        //this.baseMapper.insert(weChatPay);

        log.info("业务订单号,outTradeNo:{}", outTradeNo);
        //todo 把微信支付后需要处理的具体业务处理了
    }

    /**
     * 获取证书序列号
     *
     * @return String
     */
    private String getSerialNumber() {
        log.info("验证证书路径:{}", wechatConfigProperties.getApiClientCertPath());
        // 获取证书序列号
        X509Certificate certificate = PayKit.getCertificate(FileUtil.getInputStream(wechatConfigProperties.getApiClientCertPath()));
        String serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
        log.info("获取证书序列号:{},", serialNo);
        return serialNo;
    }

    /**
     * 申请退款
     *
     * @return String
     */
    public String payRefund(String outTradeNo) {
        logger.info("进入退款接口------>" );

        logger.info("执行操作的 原支付交易对应的商户订单号:{}", outTradeNo );
        try {
            String outRefundNo = PayKit.generateStr();
            logger.info("商户退款单号: {}", outRefundNo);
            List<RefundGoodsDetail> list = new ArrayList<>();
            RefundGoodsDetail refundGoodsDetail = new RefundGoodsDetail()
                    .setMerchant_goods_id( outRefundNo )
                    .setGoods_name( "取消订单" )
                    .setUnit_price( 1 )     //金额,单位为分
                    .setRefund_amount( 1 )
                    .setRefund_quantity(1);
            list.add(refundGoodsDetail);
            RefundModel refundModel = new RefundModel()
                    .setOut_refund_no(outRefundNo)
                    .setReason("取消订单")
                    //.setNotify_url(wechatPayV3Bean.getDomain().concat("/wechat/operation/pay/refundNotify")) //退款异步回调通知
                    .setAmount(new RefundAmount().setRefund(1).setTotal(1).setCurrency("CNY"))
                    .setGoods_detail(list);
            if (outTradeNo!=null && !outTradeNo.equals("")) {
                refundModel.setOut_trade_no( outTradeNo );
            }
            logger.info("退款参数: {}", JSONUtil.toJsonStr(refundModel));
            IJPayHttpResponse response = WxPayApi.v3(
                    RequestMethodEnum.POST,
                    //中国境内的微信支付环境
                    WxDomainEnum.CHINA.toString(),
                    //微信退款API接口
                    BasePayApiEnum.DOMESTIC_REFUND.toString(),
                    wechatConfigProperties.getMchId(),
                    //获取证书序列号
                    getSerialNumber(),
                    null,
                    wechatConfigProperties.getApiClientKeyPath(),
                    JSONUtil.toJsonStr(refundModel)
            );
            logger.info("退款响应 {}", response);
            // 根据证书序列号查询对应的证书来验证签名结果
            boolean verifySignature = WxPayKit.verifySignature(response, wechatConfigProperties.getPlatformCertPath());
            logger.info("verifySignature: {}", verifySignature);
            String body = response.getBody();
            JSONObject jsonObject = JSONUtil.parseObj(body); //转换为JSON
            if(response.getStatus()==200){ //退款成功,处理业务逻辑
              // todo 微信退款成功,处理业务逻辑
                //return response.getBody();
            }
            if (verifySignature) {
                return response.getBody();
            }
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        }
        return null;

    }
}

注意openId 是微信登录时候返回的标识,当前登录账户与付款账户需要为同一人。

关于微信登录换取openId 可以参考笔者下文的方法,也可直接参考微信开发文档。

public static Map<String, String> getUnionId2(String code, String appid, String secret) {
        String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code";
        String res = HttpUtils.sendGet(url, null);
        JSONObject jsonObject = JSON.parseObject(res);
        HashMap<String, String> map = new HashMap<>();
        System.out.println("========" + jsonObject);
        String unionid = jsonObject.get("unionid").toString();
        String openid = jsonObject.get("openid").toString();
        map.put("unionid",unionid);
        map.put("openid",openid);
        return map;
    }

四、总结

   笔者实现的小程序支付是一个简单的支付流程(下单,回调通知,退款,查询账单)。

   实际的扣款操作是前端调用微信的SDK或API进行支付,后台处理的是下单的订单信息,通过提供给微信的回调接口获取支付通知的过程。

 关于接口传参和返回值的具体信息可以参考:

微信支付-开发者文档 (qq.com)

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值