Bootstrap

java对接聚合支付(计全付)

背景

项目中本来是使用微信和支付宝分开的收款码收款,现在需要实现一张二维码图片,微信/支付宝 都能扫码付款
在这里插入图片描述
这里可以先看看自己支付宝或者微信的费率,我们公司直接对接支付宝和微信的费率分别的0.6%和0.9%,后来了解到一些聚合支付的通道能给到0.38%,真香。
开始选型,要求是文档清晰,长期稳定,费率低,安全可靠。
这里选的是【计全付】,开始找了一些其它的要么费率高,不靠谱,弄得我想自己写个聚合支付了,在gitee找代码时看到的,star还挺高的就看了下
计全付码云仓库
SDK地址
jeepay-plus文档
文档还算详尽,这里做个记录

准备工作,注册获取开发参数

注册地址
在这里插入图片描述
注册–>登录
在这里插入图片描述
目前商家推荐使用盛付通

在这里插入图片描述
获取商户号
在这里插入图片描述
获取APPID
在这里插入图片描述
在这里插入图片描述
获取私钥

开发

引入SDK

        <dependency>
            <groupId>com.jeequan</groupId>
            <artifactId>jeepay-sdk-java</artifactId>
            <version>1.5.0</version>
        </dependency>

为了方便统一管理,添加配置文件
在这里插入图片描述
回调地址是自己本地的,后面会用到

调用SDK每次都需要new一个JeepayClient,并赋值,这里为了方便,写在配置文件里面


import com.jeequan.jeepay.Jeepay;
import com.jeequan.jeepay.JeepayClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

/**
 * @author:xianyu
 * @createDate:2022/8/5
 * @description:
 */
@Configuration
@PropertySource("classpath:jee-pay.properties")
public class JeePayClientConfig {

    @Autowired
    private Environment config;

    @Bean
    public JeepayClient jeePayConfig(){

        Jeepay.setApiBase(config.getProperty("api-base"));
        Jeepay.apiKey = config.getProperty("api-key");
        Jeepay.mchNo = config.getProperty("mch-no");
        Jeepay.appId = config.getProperty("app-id");

        JeepayClient jeepayClient = JeepayClient.getInstance(Jeepay.appId, Jeepay.apiKey, Jeepay.getApiBase());

        return jeepayClient;
    }
}

注意classpath:后面接自己配置文件的名称,按住ctrl能跳转就说明没问题

下单接口

编写扫码下单接口,这里实体类就不贴出了,可参考开发文档里面自定义参数即可

controller


import com.jeequan.jeepay.exception.JeepayException;
import com.mcsgis.saas.common.data.R;
import com.mcsgis.saas.system.api.domain.dto.OrderInfoDto;
import com.mcsgis.saas.yun.service.JeePayService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * @author:xianyu
 * @createDate:2022/8/5
 * @description:
 */
@Api(tags = "计全聚合支付")
@RestController()
public class JeePayController {

    @Autowired
    private JeePayService jeePayService;

    /**
     * 扫码下单
     *
     * @param orderInfoDto
     * @return
     * @throws JeepayException
     */
    @PostMapping("/scanPay")
    public String scanPay(@RequestBody OrderInfoDto orderInfoDto) throws JeepayException {
        return jeePayService.scanPay(orderInfoDto);
    }
}

service

    /**
     * 统一生成订单接口,返回二维码
     *
     * @param orderInfoDto
     * @return
     * @throws JeepayException
     */
    String scanPay(OrderInfoDto orderInfoDto) throws JeepayException;

serviceImpl


import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.jeequan.jeepay.Jeepay;
import com.jeequan.jeepay.JeepayClient;
import com.jeequan.jeepay.exception.JeepayException;
import com.jeequan.jeepay.model.PayOrderCreateReqModel;
import com.jeequan.jeepay.request.PayOrderCreateRequest;
import com.jeequan.jeepay.response.PayOrderCreateResponse;
import com.jeequan.jeepay.util.JeepayKit;
import com.mcsgis.saas.common.constant.JeePayConstants;
import com.mcsgis.saas.common.util.JsonUtil;
import com.mcsgis.saas.system.api.domain.dto.OrderInfoDto;
import com.mcsgis.saas.yun.configuration.JeePayClientConfig;
import com.mcsgis.saas.yun.service.JeePayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import sun.security.krb5.internal.APRep;

import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author:xianyu
 * @createDate:2022/8/5
 * @description:
 */
@Slf4j
@Service
public class JeePayServiceImpl implements JeePayService {

    @Autowired
    private JeepayClient jeepayClient;

    @Autowired
    private Environment config;

    @Override
    public String scanPay(OrderInfoDto orderInfoDto) {

        // 构建请求数据
        PayOrderCreateRequest request = new PayOrderCreateRequest();
        PayOrderCreateReqModel model = new PayOrderCreateReqModel();
        // 商户号
        model.setMchNo(Jeepay.mchNo);
        // 应用ID
        model.setAppId(Jeepay.appId);
        // 商户订单号
        model.setMchOrderNo(orderInfoDto.getOutTradeNo());
        // 支付方式
        model.setWayCode(JeePayConstants.QR_CASHIER);
        // 金额,单位分
        long amount = orderInfoDto.getTotalAmount().multiply(new BigDecimal(100)).longValue();
        model.setAmount(amount);
        // 币种,目前只支持cny
        model.setCurrency("CNY");
        // 发起支付请求客户端的IP地址
        model.setClientIp(config.getProperty("ip-address"));
        // 商品标题
        model.setSubject(orderInfoDto.getSubject());
        // 商品描述
        model.setBody("商品描述");
        // 异步通知地址
        model.setNotifyUrl(config.getProperty("notify"));
        // 前端跳转地址
        model.setReturnUrl("");
        // 渠道扩展参数
        model.setChannelExtra(channelExtra(JeePayConstants.QR_CASHIER));
        // 商户扩展参数,回调时原样返回
        model.setExtParam("");
        request.setBizModel(model);
        log.info("jeepay下单参数处理完毕,参数:[{}]", JSON.toJSONString(request));
        String result = "failure";
        try {
            PayOrderCreateResponse response = jeepayClient.execute(request);
            // 下单成功
            if (response.isSuccess(Jeepay.apiKey)) {
                result = response.getData().getString("payData");
            } else {
                log.warn("下单失败:{}", orderInfoDto.getOutTradeNo());
            }
        } catch (JeepayException e) {
            log.error(e.getMessage());
        }
        return result;
    }
    String channelExtra(String wayCode) {
        if ("WX_LITE".equals(wayCode)) return wxJsapiExtra();
        if ("WX_JSAPI".equals(wayCode)) return wxJsapiExtra();
        if ("WX_BAR".equals(wayCode)) return wxBarExtra();
        if ("ALI_BAR".equals(wayCode)) return aliBarExtra();
        if ("YSF_BAR".equals(wayCode)) return ysfBarExtra();
        if ("UPACP_BAR".equals(wayCode)) return upacpBarExtra();
        if ("QR_CASHIER".equals(wayCode)) return qrCashierExtra();
        if ("AUTO_BAR".equals(wayCode)) return autoBarExtra();
        if ("PP_PC".equals(wayCode)) return ppExtra();
        if ("SAND_H5".equals(wayCode)) return sandH5Extra();
        return "";
    }

    private String wxJsapiExtra() {
        JSONObject obj = new JSONObject();
        obj.put("openId", "134756231107811344");
        return obj.toString();
    }

    private String wxBarExtra() {
        JSONObject obj = new JSONObject();
        obj.put("authCode", "134675721924600802");
        return obj.toString();
    }

    private String aliBarExtra() {
        JSONObject obj = new JSONObject();
        obj.put("authCode", "1180812820366966512");
        return obj.toString();
    }

    private String ysfBarExtra() {
        JSONObject obj = new JSONObject();
        obj.put("authCode", "6223194037624963090");
        return obj.toString();
    }

    private String upacpBarExtra() {
        JSONObject obj = new JSONObject();
        obj.put("authCode", "6227662446181058584");
        return obj.toString();
    }

    private String qrCashierExtra() {
        JSONObject obj = new JSONObject();
        obj.put("payDataType", "codeImgUrl");
        return obj.toString();
    }

    private String autoBarExtra() {
        JSONObject obj = new JSONObject();
        obj.put("authCode", "134753177301492386");
        return obj.toString();
    }

    private String ppExtra() {
        JSONObject obj = new JSONObject();
        obj.put("cancelUrl", "http://baidu.com");
        return obj.toString();
    }

    private String sandH5Extra() {
        JSONObject obj = new JSONObject();
        JSONObject payExtra = new JSONObject();
        // 聚合码
        obj.put("productCode", "02000001");
        obj.put("payExtra", "");
        obj.put("metaOption", "[{\"s\":\"Pc\",\"n\":\"支付\"}]");
        // 微信公众号
        /*obj.put("productCode", "02010002");
        payExtra = new JSONObject();
        payExtra.put("mer_app_id", "");
        payExtra.put("openid", "");
        obj.put("payExtra", payExtra.toString());
        obj.put("metaOption", "");
        // 微信小程序(云函数sdk)
        obj.put("productCode", "02010007");
        payExtra = new JSONObject();
        payExtra.put("wx_app_id", "");  // 移动应用Appid(微信开放平台获取,wx开头)
        payExtra.put("gh_ori_id", "");  // 小程序原始id(微信公众平台获取,gh_开头)
        payExtra.put("path_url", "");   // 拉起小程序页面的可带参路径,不填默认拉起小程序首页
        payExtra.put("miniProgramType", "0");   // 开发时根据小程序是开发版、体验版或正式版自行选择。正式版:0; 开发版:1; 体验版:2
        obj.put("payExtra", payExtra.toString());
        obj.put("metaOption", "");
        // 支付宝生活号
        obj.put("productCode", "02010002");
        payExtra = new JSONObject();
        payExtra.put("buyer_id", "");  // 支付宝生活号所需参数(支付宝H5建议不传)
        obj.put("payExtra", payExtra.toString());
        obj.put("metaOption", "");*/
        return obj.toString();

    }
}

在配置文件中给 JeepayClient 初始值了,这里直接用@Autowired引入即可,不需要额外配置

测试下单接口,返回值

{
“status”: 200,
“msg”: “操作成功”,
“data”: “https://pay.jeepay.vip/api/scan/imgs/a034b9a3cc3e89d53d936c196d5a48c219c1f01a8623733b004429d057af027e69b4220ccdc8ad29375295f30ccb3e1cf130261adeb28c64f27dc682305ec082062889a910c12dba1317fa3f69c29eb0c2c83f71516c305b25830bf4a83e21abb235934310960a27aef6f6868c4a1481c52f213b693c30c0c391ecd371e17e0bfbc0f77203beabcb7549c44df66de99d06ad9d731e2ebac14a43effcec80aace.png”
}

返回的是一个图片地址,复制到浏览器中
在这里插入图片描述

微信扫码测试
在这里插入图片描述

支付宝扫码测试
在这里插入图片描述

回调接口

业务系统处理后同步返回给支付中心,返回字符串 success 则表示成功,返回非success则表示处理失败,支付中心会再次通知业务系统。(通知频率为0/30/60/90/120/150,单位:秒)

支付完成之后,我们的业务系统遇到知道用户已经付款,这里就有一个要使用回调,第四方商家通知我们的业务系统,我们开发普遍都在内网,这里就需要使用一个内网穿透,我用的是 ngrok

回调接口

    @ApiOperation("支付回调")
    @PostMapping("/tradeNotify")
    public String tradeNotify(HttpServletRequest req) {
        return orderInfoService.tradeNotify(req);
    }
@Override
    public String tradeNotify(HttpServletRequest req) {
        String result = "failure";
        try {
            Map<String, Object> map = getParamsMap(req);
            log.info("支付回调参数处理完毕,参数:[{}]", JSON.toJSONString(map));
            //获取私钥
            String s = redisTemplate.opsForValue().get(SysConfigConstants.JEE_PAY);
            JSONObject configObj = new JSONObject();
            String apikey;
            if (StrUtil.isNotBlank(s)) {
                configObj = JSONObject.parseObject(s);
                apikey = configObj.get("api-key").toString();
            } else {
                SysConfiguration configuration = sysConfigurationMapper.findByKey(SysConfigConstants.JEE_PAY);
                Object config = configuration.getConfig();
                redisTemplate.opsForValue().set(SysConfigConstants.JEE_PAY, config.toString(), SMS_EXPIRES, TimeUnit.MILLISECONDS);
                configObj = JSONObject.parseObject(config.toString());
                apikey = configObj.get("api-key").toString();
            }

            //验签
            if (chackSgin(map, apikey)) {
                return result;
            }
            log.info("回调验签处理完成,开始处理业务逻辑,参数:[{}]", JSON.toJSONString(map));
            OrderInfo info = new OrderInfo();
            info.setOutTradeNo(map.get("mchOrderNo").toString());
            info.setIsPay(true);
            updateByPrimaryKeySelective(info);

            result = "success";
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

我这里的私钥放在数据库中,放在配置文件的不用可以直接取,不用查库或者redis

验签

    /**
     * 回调验签
     *
     * @param result
     * @param map
     * @param apikey
     * @return
     */
    private Boolean chackSgin(Map<String, Object> map, String apikey) {

        Object sign = map.remove("sign");
        String reSign = JeepayKit.getSign(map, apikey);
        log.info("调用SDK加签,返回参数:[{}]", reSign);

        if (!Objects.equals(reSign, sign)) {
            log.error("支付成功,异步通知验签失败!");
            return true;
        }

        log.info("支付成功,异步通知验签成功!");
        //TODO 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验
        //1.验证mchOrderNo 是否为商家系统中创建的订单号
        List<OrderInfo> orderInfos = orderInfoMapper.selectByOutTradeNo(map.get("mchOrderNo").toString());
        log.info("支付成功回调,查询订单,[{}]", JSON.toJSONString(orderInfos));
        if (CollectionUtil.isEmpty(orderInfos)) {
            log.error("支付成功,回调通知,mchOrderNo不是本系统生成的订单号!!");
            return true;
        }

        //2.判断 amountt 是否确实为该订单的实际金额
        //订单金额单位转换为分
        BigDecimal reduce = orderInfos.stream().map(e -> e.getPrice()).reduce(new BigDecimal("0"), BigDecimal::add).multiply(new BigDecimal(100));
        BigDecimal amount = new BigDecimal(map.get("amount").toString());
        if (reduce.compareTo(amount) != 0) {
            log.error("支付成功,回调通知,订单金额与实际金额不符!!");
            return true;
        }
        //todo 后续添加 2022-08-16
        //3.校验通知中的 mchNo 是否是本条
        //4.验证 app_id 是否为该商家本身

        return false;
    }

    private Map<String, Object> getParamsMap(HttpServletRequest req) {
        Map<String, String[]> requestMap = req.getParameterMap();
        Map<String, Object> paramsMap = new HashMap<>();
        requestMap.forEach((key, values) -> {
            String strs = "";
            for (String value : values) {
                strs = strs + value;
            }
            paramsMap.put(key, strs);
        });
        return paramsMap;
    }

这里要说下验签,文档中给出的签名算法如下
在这里插入图片描述
这里只做了支付平台的比对,支付宝的文档中还需要与本地系统对比做二次校验

验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验
1.验证mchOrderNo 是否为商家系统中创建的订单号
2.判断 amountt 是否确实为该订单的实际金额
3.校验通知中的 mchNo 是否是本条
4.验证 app_id 是否为该商家本身

只要这4条有一条不对就不能返回success

查询订单接口

    /**
     * 查询订单状态
     *
     * @param outTradeNo
     * @return 0-订单生成 1-支付中 2-支付成功 3-支付失败 4-已撤销 5-已退款 6-订单关闭
     * @throws Exception
     */
    @ApiOperation("查询订单状态")
    @GetMapping("/query")
    public R query(@RequestParam("outTradeNo") @ApiParam("订单编号(唯一)") String outTradeNo){
        Map map = jeePayService.query(outTradeNo);
        return R.success(map);
    }
@Override
    public Map query(String outTradeNo) {
        log.info("查询订单状态,outTradeNo:{}",outTradeNo);
        Map<String,Object> parMap =new HashMap<>();
        parMap.put("mchNo",config.getProperty("mch-no"));
        parMap.put("appId",config.getProperty("app-id"));
        parMap.put("mchOrderNo",outTradeNo);
        parMap.put("reqTime",new Date().getTime());
        parMap.put("version","1.0");
        parMap.put("signType","MD5");
        String sign = JeepayKit.getSign(parMap, config.getProperty("api-key"));
        parMap.put("sign",sign);

        log.info("查询订单状态,参数处理完毕,参数:[{}]",JSON.toJSONString(parMap));
        String body = HttpRequest.post(config.getProperty("query-url"))
                .body(JSON.toJSONString(parMap))
                .execute()
                .body();
        Map parse = JsonUtil.parse(body, Map.class);
        Map dataMap =(Map) parse.get("data");
        log.info("查询订单状态,完成,返回结果:[{}]",JSON.toJSONString(dataMap));

        return dataMap;
    }

这个接口比较简单,不过有个BUG,截止我写完博客也没解决,返回值没有4-已撤销,支付的时候取消了,返回值还是1-支付中,计全付商家给出的解释是支付状态是上游返回,他们也只是原样返回给我这边,不过我自己对接支付宝或者微信的时候没有出现这种情况

;