Bootstrap

支付宝和微信支付对接流程

支付宝简介

使用流程

支付宝开放平台 (alipay.com)

  • 1、登录支付宝开放平台-创建应用
    • 得到app_id
  • 2、绑定应用:在产品绑定中,绑定支付宝的相关功能
  • 3、配置应用:
    • 配置各种加密方式
  • 4、提交审核:
  • 5、把支付宝整个功能整合项目

沙箱:

  • 支付宝提前为我们创建好一个账号,用这个账号可以体验支付宝的所有功能;
  • 应用开发期间用沙箱账号,上线以后替换成线上账号即可。

产品介绍 - 支付宝文档中心 (alipay.com):电脑网站支付的所有文档

沙箱

下载密钥生成工具:密钥工具下载 - 支付宝文档中心 (alipay.com)

按照官方教程去生成自己的私钥。

支付宝整体应用流程

配置properties,方便配置支付宝appid,私钥密钥等

@Data
@ConfigurationProperties(prefix = "app.pay.alipay")
public class AlipayProperties {

    // 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
    private String app_id ;

    // 商户私钥,您的PKCS8格式RSA2私钥
    private String merchant_private_key;

    // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
    private String alipay_public_key;

    // 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    private String notify_url ;

    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    private String return_url;

    // 签名方式
    private String sign_type;

    // 字符编码格式
    private String charset;

    // 支付宝网关
    private String gatewayUrl;
}

在config类中放一个AlipayClient。避免每次调用都需要专门new一个。

@EnableConfigurationProperties(AlipayProperties.class)
@Configuration
public class AlipayConfig {

    @Bean
    AlipayClient alipayClient(AlipayProperties alipayProperties){
        return new DefaultAlipayClient( alipayProperties.getGatewayUrl(),
                alipayProperties.getApp_id(),
                alipayProperties.getMerchant_private_key(),
                "json",
                alipayProperties.getCharset(),
                alipayProperties.getAlipay_public_key(),
                alipayProperties.getSign_type());
    }

}

在payService实现生成支付页的方法

    @Override
    public String generatePayPage(Long orderId, Long userId) throws AlipayApiException {
        //创建一个AlipayClient

        //创建一个支付请求
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(alipayProperties.getReturn_url());//同步回调 支付成功后浏览器跳转到的地址
        alipayRequest.setNotifyUrl(alipayProperties.getNotify_url());//通知回调 支付成功后通知的地址

        //准备待支付的订单数据
        //远程调用订单服务获取其基本信息 基于此数据生成订单页
        OrderInfo orderInfo = orderFeignClient.getOrderInfoById(orderId).getData();

        //商户订单号,商户网站订单系统中唯一订单号,必填
        String outTradeNo = orderInfo.getOutTradeNo();
        //付款金额,必填
        BigDecimal totalAmount =orderInfo.getTotalAmount();
        //订单名称,必填
        String orderName = "尚品汇-订单-"+outTradeNo;
        //商品描述,可空
        String tradeBody = orderInfo.getTradeBody();

        Map<String,Object> bizContent= new HashMap<>();
        bizContent.put("out_trade_no",outTradeNo);
        bizContent.put("total_amount",totalAmount);
        bizContent.put("subject",orderName);
        bizContent.put("body",tradeBody);
        bizContent.put("product_code","FAST_INSTANT_TRADE_PAY");
        alipayRequest.setBizContent(JSON.toJSONString(bizContent));

        //请求
        String page = alipayClient.pageExecute(alipayRequest).getBody();

        return page;
    }

需要一个方法去接受支付宝支付成功的回调。此处将其放入mq消息队列中。等待消费。修改订单状态

    /**
     * 支付成功后支付宝会给这里发送支付结果通知 异步
     * @param params
     * @return
     */
    @PostMapping("/notify/success")
    public String paySuccessNotify(@RequestParam Map<String,String> params) throws AlipayApiException {
        log.info("收到支付宝支付消息通知:{}", JSON.toJSONString(params));

        //验证签名
        boolean signVerified = AlipaySignature.rsaCheckV1(params,
                alipayProperties.getAlipay_public_key(),
                alipayProperties.getCharset(),
                alipayProperties.getSign_type());//调用SDK验证签名
        if(signVerified){
            log.info("验签通过,准备修改订单状态");
            String trade_status = params.get("trade_status");
            if("TRADE_SUCCESS".equals(trade_status)){
                //修改订单状态 通过消息传递机制
                mqService.send(params, MqConst.ORDER_EVENT_EXCHANGE,MqConst.ORDER_PAYED_RK);
            }
        }
        //什么时候给支付宝返回success
        return "success";
    }

修改订单状态 如果用户在临关单前的极限时间支付后,为了避免用户订单被强制改为关单。我们需要设置较高的优先级。

    @Override
    public void payedOrder(String outTradeNo, Long userId) {
        //关单消息和支付消息同时抵达的话,以支付为准,将其改为已支付
        //订单是未支付或是已关闭都可以改为已支付
        ProcessStatus payed = ProcessStatus.PAID;
        //修改订单状态为已支付
        boolean update = orderInfoService.lambdaUpdate()
                .set(OrderInfo::getOrderStatus, payed.getOrderStatus().name())
                .set(OrderInfo::getProcessStatus, payed.name())
                .eq(OrderInfo::getUserId, userId)
                .eq(OrderInfo::getOutTradeNo, outTradeNo)
                .in(OrderInfo::getOrderStatus, OrderStatus.UNPAID.name(), OrderStatus.CLOSED.name())
                .in(OrderInfo::getProcessStatus, ProcessStatus.UNPAID.name(), ProcessStatus.CLOSED.name())
                .update();

        log.info("修改订单:{} 状态为已支付成功:{}",outTradeNo,update);
    }

修改状态后,将payment_info也存入数据

  /**
     * 监听所有成功单队列
     */
    @RabbitListener(queues = MqConst.ORDER_PAYED_QUEUE)
    public void listen(Message message, Channel channel) throws IOException {
        long tag = message.getMessageProperties().getDeliveryTag();
        String json = new String(message.getBody());
        try {

            Map<String, String> content = JSON.parseObject(json, new TypeReference<Map<String, String>>() {
            });
            log.info("修改订单状态为已支付");
            //订单的唯一对外交易号
            String out_trade_no = content.get("out_trade_no");
            //知道用户id
            String[] split = out_trade_no.split("-");
            Long userId = Long.parseLong(split[split.length - 1]);

            //根据唯一对外交易号和用户id修改
            orderBizService.payedOrder(out_trade_no,userId);

            PaymentInfo info = preparePaymentInfo(json, content, out_trade_no, userId);

            paymentInfoService.save(info);

            channel.basicAck(tag,false);
        } catch (NumberFormatException | IOException e) {
            mqService.retry(channel,tag,json,5);
        }

    }

    private PaymentInfo preparePaymentInfo(String json, Map<String, String> content, String out_trade_no, Long userId) {
        //保存此次支付的回调信息到payment_info里
        PaymentInfo info = new PaymentInfo();

        //查询orderInfo
        OrderInfo orderInfo = orderInfoService.lambdaQuery()
                .eq(OrderInfo::getOutTradeNo, out_trade_no)
                .eq(OrderInfo::getUserId, userId).one();
        info.setOutTradeNo(out_trade_no);
        info.setUserId(userId);
        info.setOrderId(orderInfo.getId());
        info.setPaymentType(orderInfo.getPaymentWay());
        //支付宝给的流水号
        String trade_no = content.get("trade_no");
        info.setTradeNo(trade_no);
        String total_amount = content.get("total_amount");
        info.setTotalAmount(new BigDecimal(total_amount));

        info.setSubject(content.get("subject"));
        info.setPaymentStatus(content.get("trade_status"));
        info.setCreateTime(new Date());
        info.setCallbackTime(new Date());
        info.setCallbackContent(json);
        return info;
    }

微信支付

微信支付V2

准备环境

C扫B需求要求可以手机网页交互,微信JSAPI/NATIVE支付符合需求。

总结步骤:

  1. 微信支付商户平台申请支付商户,填写企业资质认证后,可以在用户信息中获取 商户号
  2. 以企业身份注册微信公众号 https://mp.weixin.qq.com/
  3. 登录公众号,点击左侧菜单 “微信支付” 开通微信支付,需要提供营业执照、身份证等信息。
  4. 缴纳认证费用,等待1-3工作日审核。
  5. 审核通过后在支付设置中获取 APPID
  6. 审核通过后需要在 微信支付商户平台 中绑定APPID
  7. 绑定成功后需要申请 支付相关API秘钥
快速入门

参考文档:https://pay.weixin.qq.com/wiki/doc/api/index.html

支付产品: https://pay.weixin.qq.com/static/product/product_index.shtml

退款:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_4

微信支付目前有两个大版本是公司使用的分别是V2 和 V3 版本,旧项目-V2 新项目-V3

V3新版本:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml

官方SDK与DEMO代码:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1

开发流程:

在这里插入图片描述

注意:demo用的是微信V2版本,实际项目用的是微信V3版本

NATIVE代码实现:

(1)添加依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>com.github.tedzhdz</groupId>
  <artifactId>wxpay-sdk</artifactId>
  <version>3.0.10</version>
</dependency>

(2)编写配置类

package com.itheima.pay.config;
import com.github.wxpay.sdk.IWXPayDomain;
import com.github.wxpay.sdk.WXPayConfig;
import com.github.wxpay.sdk.WXPayConstants;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
public class WXPayConfigCustom extends WXPayConfig {
    /**
     * 开发者ID(AppID)
     * @return
     */
    @Override
    protected String getAppID() {
        return "wx0ca99a203b1e9943";
    }
    /**
     * 商户号
     * @return
     */
    @Override
    protected String getMchID() {
        return "1561414331";
    }
    /**
     * appkey  API密钥
     * @return
     */
    @Override
    protected String getKey() {
        return "CZBK51236435wxpay435434323FFDuis";
    }
  // 退款:必须强制使用API证书
    @Override
    protected InputStream getCertStream() {
        try {
            String path = ClassLoader.getSystemResource("").getPath();
            return new FileInputStream(new File(path+"apiclient_cert.p12"));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
    @Override
    protected IWXPayDomain getWXPayDomain() {
        return new IWXPayDomain() {
            @Override
            public void report(String s, long l, Exception e) {
            }
            @Override
            public DomainInfo getDomain(WXPayConfig wxPayConfig) {
                return new DomainInfo(WXPayConstants.DOMAIN_API, true);
            }
        };
    }
}

注意:

  • 退款操作需要API证书,将资料中的证书(apiclient_cert.p12)放到resources路径下

(3)编写下单测试方法

package com.itheima.pay.controller;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayUtil;
import com.itheima.pay.config.WXPayConfigCustom;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
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;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
 * @Description:
 * @Version: V1.0
 */
@Slf4j
@RestController
@RequestMapping("wxpay")
public class WxpayController {
    /**
     * 支付回调通知
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("notify")
    public String payNotify(HttpServletRequest request, HttpServletResponse response) {
        try {
            String xmlResult = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
            Map<String, String> map = WXPayUtil.xmlToMap(xmlResult);
            // 加入自己处理订单的业务逻辑,需要判断订单是否已经支付过,否则可能会重复调用
            String orderId = map.get("out_trade_no");
            String tradeNo = map.get("transaction_id");
            String totalFee = map.get("total_fee");
            String returnCode = map.get("return_code");
            String resultCode = map.get("result_code");
            return WxPayNotifyResponse.success("处理成功!");
        } catch (Exception e) {
            log.error("微信回调结果异常,异常原因{}", e.getMessage());
            return WxPayNotifyResponse.fail(e.getMessage());
        }
    }
    /**
     * 下单操作
     * @param code
     * @return
     * @throws Exception
     */
    @GetMapping("unifiedOrder/{code}")
    public String unifiedOrder(@PathVariable String code) throws Exception {
        WXPayConfigCustom config = new WXPayConfigCustom();
        WXPay wxpay = new WXPay(config);
        Map<String, String> data = new HashMap<String, String>();
        data.put("body", "餐掌柜-餐饮消费");
//        data.put("out_trade_no", "2138091910595900001012");
        data.put("out_trade_no", code);
        data.put("device_info", "");
        data.put("fee_type", "CNY");
        data.put("total_fee", "1");
        data.put("spbill_create_ip", "123.12.12.123");
        data.put("notify_url", "http://itheima.ngrok2.xiaomiqiu.cn/wxpay/notify");
        data.put("trade_type", "NATIVE");  // NATIVE 指定为扫码支付  JSAPI 网站支付
//        data.put("openid", "12");
        try {
            Map<String, String> resp = wxpay.unifiedOrder(data);
            System.out.println("支付结果:"+resp);
            return resp.get("code_url");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "OK";
    }
    /**
     * 退款
     * @param code 订单号
     * @param refund_no 退款号
     * @return
     * @throws Exception
     */
    @GetMapping("refunds/{code}/{refund_no}")
    public Map<String, String> refunds(@PathVariable String code, @PathVariable String refund_no) throws Exception {
        WXPayConfigCustom config = new WXPayConfigCustom();
        WXPay wxpay = new WXPay(config);
        Map<String, String> data = new HashMap<String, String>();
        data.put("out_trade_no", code);
        data.put("out_refund_no", refund_no);
        data.put("notify_url", "http://484cd438.cpolar.io/wxpay/notify");
        data.put("refund_desc", "已经售罄");
        data.put("refund_fee", "1");
        data.put("total_fee", "1");
        data.put("refund_fee_type", "CNY");
        System.out.println("请求参数:" + data);
        Map<String, String> map = wxpay.refund(data);
        return map;
    }
}

微信支付V3

V3版本。文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/index.shtml

微信官方并没有提供类似支付宝的EasySDK,只提供了基于HttpClient封装的SDK包,在项目中我们对于此SDK做了二次封装。微信接口都是基于RESTful进行提供的。


/**
 * 微信支付远程调用对象
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WechatPayHttpClient {
    private String mchId; //商户号
    private String appId; //应用号
    private String privateKey; //私钥字符串
    private String mchSerialNo; //商户证书序列号
    private String apiV3Key; //V3密钥
    private String domain; //请求域名
    private String notifyUrl; //请求地址
    public static WechatPayHttpClient get(Long enterpriseId) {
        // 查询配置
        PayChannelService payChannelService = SpringUtil.getBean(PayChannelService.class);
        PayChannelEntity payChannel = payChannelService.findByEnterpriseId(enterpriseId, TradingConstant.TRADING_CHANNEL_WECHAT_PAY);
        if (ObjectUtil.isEmpty(payChannel)) {
            throw new SLException(TradingEnum.CONFIG_EMPTY);
        }
        //通过渠道对象转化成微信支付的client对象
        JSONObject otherConfig = JSONUtil.parseObj(payChannel.getOtherConfig());
        return WechatPayHttpClient.builder()
                .appId(payChannel.getAppId())
                .domain(payChannel.getDomain())
                .privateKey(payChannel.getMerchantPrivateKey())
                .mchId(otherConfig.getStr("mchId"))
                .mchSerialNo(otherConfig.getStr("mchSerialNo"))
                .apiV3Key(otherConfig.getStr("apiV3Key"))
                .notifyUrl(payChannel.getNotifyUrl())
                .build();
    }
    /***
     * 构建CloseableHttpClient远程请求对象
     * @return org.apache.http.impl.client.CloseableHttpClient
     */
    public CloseableHttpClient createHttpClient() throws Exception {
        // 加载商户私钥(privateKey:私钥字符串)
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8)));
        // 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, merchantPrivateKey);
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
        // 向证书管理器增加需要自动更新平台证书的商户信息
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
        // 初始化httpClient
        return com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, merchantPrivateKey)
                .withValidator(new WechatPay2Validator(certificatesManager.getVerifier(mchId)))
                .build();
    }
    /***
     * 支持post请求的远程调用
     *
     * @param apiPath api地址
     * @param params 携带请求参数
     * @return 返回字符串
     */
    public WeChatResponse doPost(String apiPath, Map<String, Object> params) throws Exception {
        String url = StrUtil.format("https://{}{}", this.domain, apiPath);
        HttpPost httpPost = new HttpPost(url);
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type", "application/json; charset=utf-8");
        String body = JSONUtil.toJsonStr(params);
        httpPost.setEntity(new StringEntity(body, CharsetUtil.UTF_8));
        CloseableHttpResponse response = this.createHttpClient().execute(httpPost);
        return new WeChatResponse(response);
    }
    /***
     * 支持get请求的远程调用
     * @param apiPath api地址
     * @param params 在路径中请求的参数
     * @return 返回字符串
     */
    public WeChatResponse doGet(String apiPath, Map<String, Object> params) throws Exception {
        URI uri = UrlBuilder.create()
                .setHost(this.domain)
                .setScheme("https")
                .setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8))
                .setQuery(UrlQuery.of(params))
                .setCharset(CharsetUtil.CHARSET_UTF_8)
                .toURI();
        return this.doGet(uri);
    }
    /***
     * 支持get请求的远程调用
     * @param apiPath api地址
     * @return 返回字符串
     */
    public WeChatResponse doGet(String apiPath) throws Exception {
        URI uri = UrlBuilder.create()
                .setHost(this.domain)
                .setScheme("https")
                .setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8))
                .setCharset(CharsetUtil.CHARSET_UTF_8)
                .toURI();
        return this.doGet(uri);
    }
    private WeChatResponse doGet(URI uri) throws Exception {
        HttpGet httpGet = new HttpGet(uri);
        httpGet.addHeader("Accept", "application/json");
        CloseableHttpResponse response = this.createHttpClient().execute(httpGet);
        return new WeChatResponse(response);
    }
}

代码说明:

  • 通过get(Long enterpriseId)方法查询商户对应的配置信息,最后封装到WechatPayHttpClient对象中。
  • 通过createHttpClient()方法封装了请求微信接口必要的参数,最后返回CloseableHttpClient对象。
  • 封装了doGet()、doPost()方便对微信接口进行调用。
        try {
            WeChatResponse response = client.doPost(apiPath, params);
            if(!response.isOk()){
                throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
            }

            tradingEntity.setPlaceOrderCode(Convert.toStr(response.getStatus())); //返回的编码
            tradingEntity.setPlaceOrderMsg(JSONUtil.parseObj(response.getBody()).getStr("code_url")); //二维码需要展现的信息
            tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response));
            tradingEntity.setTradingState(TradingStateEnum.FKZ);
        } catch (Exception e) {
            throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
        }

PS:接收回调地址的话需要用到内网穿透工具,如果没开内网穿透则无法接收到。推荐用cpolar或者natapp

;