【来C站:一起聊java】用java实现微信支付功能的详细设计思路

由于上一个项目的小程序支付模块的历练,让我意识到支付确实是一个复杂且测试起来需要的配置特别复杂的模块,这么说吧,学生想要实打实的测试微信支付太难了,它需要你有企业的相关证明,营业执照呀,公众号商户号,不论资质是否满足办理条件,单论开户费用就是几百块的收,但是微信和支付宝都提供了沙箱环境,对于沙箱这个东西,我真的没什么话说!bug很多,大家经历过的可以喷一喷。所幸甲方提供了开发所需要的一些参数与配置(商户号的一些信息,Apv3密钥与商户id,商户证书等)。那么现在不喷别的了,我们来看一些微信支付的开发文档:


一、首先需要选取商户的类型(区别最大的就是资金流的流动):

1、直连商户(即资金流与信息流直接与我们的个体商户进行流动)

2、服务商(类似于加盟形式,资金流先中转到中间的服务商商户再定时转入加盟的子商户)

需要注意的是不同的商户类型开发的模式也是有一定的区别,特别是参数需求的不同,如果误以为二者的开发模式一样,可能会导致后续的支付返回信息是参数校验不正确


二、选取好类型后我们来配置参数(这里我们以直连商户开聊):

主要的参数为:mchid(商户号),mchSerialNo(商户证书序列号),apiV3Key(商户配置的apiv3密钥),PrivateKey(证书私钥),服务器以及配置好的域名(微信后台是需要频繁与服务器进行数据交换的)。

下面是微信官方给出的文档供大家参考:

JSAPI支付-接入前准备 | 微信支付商户平台文档中心 (qq.com)https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml


三、了解流程并分析哪些事情是服务器端需要做的?哪些是小程序需要做的?

先看一下时序图(来源:微信开发者社区):

【来C站:一起聊java】用java实现微信支付功能的详细设计思路_第1张图片

 分析流程(注意这里我们用服务器来代表后端,序号不代表处理顺序):

1、小程序对服务器发起下单请求(携带业务参数如商品信息以及由wx.login()得到的临时凭证code

2、服务器接收参数,生成业务订单插入数据库记录并带着code请求微信登录API得到openidopenid是每一个微信用户使用小程序时获取的唯一身份标识,永久唯一且在登录小程序之后不变)

3、服务器根据商品信息结合openid以微信规定的参数格式生成用户订单

4、带着订单数据进行一次签名的验证之后请求支付统一下单API:

https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi

获取统一预支付会话prepay_id

5、服务器将获得的prepay_id根据微信需要的数据格式将它与其他参数加载签名之后得到sign并再次封装时间戳,sign,随机字串,小程序appid,包含prepay_id的签名,将封装之后的数据发送给小程序。

6、小程序鉴权支付之后,微信后台会发送一条支付成功的信息给服务器确认处理

7、服务器端更新业务订单的状态。


四、设计关键点的处理思路,以及根据微信官方给的文档设计签名验证请求发送封装数据生成签名获取随机字符串等工具类。

这里我处理的关键点openid的获取与prepay_id的获取:因为两次都需要请求微信后台并实现服务器端与微信后台的交互,其次服务器端的二次签名如果出现问题会产生小程序支付时参数验证错误的报错

(1)看一下获取openid我的处理代码:

/**
     * 为保护APPSecret信息,需要将这次请求放在后台进行
     * @param AppId 小程序Id
     * @param code 换取的临时凭证
     * @return 带有openId的通用对象
     */
    @RequestMapping("/WxLogin")
    @ResponseBody
    public R WeChatLogin(@RequestParam("AppId")String AppId,@RequestParam("code")String code){
        try{
            System.out.println("---------入库查询小程序secret---------");
            String servet = appInfoService.findAppServetByAppId(AppId);
            System.out.println("---------封装访问路由获取小程序唯一openId-------");
            String Url = "https://api.weixin.qq.com/sns/jscode2session" +
                    "?appid="+AppId+"&" +
                    "secret="+servet+"&" +
                    "js_code="+code+"&grant_type=authorization_code";
            System.out.println("封装路由为"+Url);
            BasicHttpClientConnectionManager connectionManager;
            connectionManager = new BasicHttpClientConnectionManager(
                    RegistryBuilder.create().
                            register("http", PlainConnectionSocketFactory.getSocketFactory())
                            .register("https", SSLConnectionSocketFactory.getSocketFactory())
                            .build(),null,null,null
            );
            CloseableHttpClient httpClient = HttpClientBuilder.create().
                    setConnectionManager(connectionManager)
                    .build();
            HttpGet httpGet = new HttpGet(Url);
            try{
                System.out.println("客户端连接成功,执行请求----------");
                HttpResponse httpResponse = httpClient.execute(httpGet);
                HttpEntity entity = httpResponse.getEntity();
                String s = EntityUtils.toString(entity, "UTF-8");
                System.out.println("请求执行结束,结果为"+ s);
                JSONObject jsonObject = new JSONObject(s);
                String openid = jsonObject.get("openid").toString();
                R r= new R<>();
                r.setData(openid);
                r.setCode(1);
                r.setMsg("登录验证");
                return r;
            }catch (Exception e){
                e.printStackTrace();
            }
        }catch (Exception e){
            return R.error("登录异常");
        }
        return R.error("登录错误");
    }

不难发现我将敏感的关键信息都放在了服务器处理,将openid返回给小程序,后面为了能够不需要在支付时再发起请求获取一次openid,我将openid存储在小程序的全局参数里以供本次会话范围内调用。(当然为了之后的登录不再访问微信后台获取openid,我也会将openid存储在数据库该用户的表里)

(2)看一下获取prepay_id我的处理代码:

/**
     * 用户普通订单支付后回调
     * 修改设备信息以及在订单记录中插入一条记录
     *
     * @param receipt 支付金额
     * @param deviceId 设备编号
     * @param userId 用户编号
     * @param useTime 使用自习室时长
     * @return 统一响应对象
     */
    @RequestMapping("/insertUserDevice")
    @ResponseBody
    /**设置事务回滚(一般用于多表操作,单表使用异常捕获就可以实现)*/
    @Transactional(rollbackFor = Exception.class)
    public R InsertUserDevice(@RequestParam("receipt") double receipt, @RequestParam("deviceId") String deviceId,
                                      @RequestParam("userId") String userId, @RequestParam("useTime") Integer useTime,
                                      @RequestParam("orderTime") String orderTime, @RequestParam("openid")String openid) {
        try {
            R r = new R<>();
            System.out.println("-------------生成订单对象---------------");
            String nonceStr = generateNonceStr();
            int payNum = new Double(receipt*100).intValue();
            String money = payNum +"";
            String order = "{"
                    + "\"amount\": {"
                    + "\"total\": "+money+","
                    + "\"currency\": \"CNY\""
                    + "},"
                    + "\"mchid\": \""+mchId+"\","
                    + "\"description\": \"自习室座位使用\","
                    + "\"notify_url\": \"https://你的服务器域名.cn/支付成功后异步确认处理接口\","
                    + "\"payer\": {"
                    + "\"openid\": \""+openid+"\"" + "},"
                    + "\"out_trade_no\": \""+nonceStr+"\","
                    + "\"appid\": \""+appid+"\"" + "}";
            System.out.println(order);
            /**封装整个付款参数*/
            String prepay_id = Tools.V3PayGet(order);
            System.out.println("获取会话id:"+prepay_id+"执行小程序二次加签");
            JSONObject jsonObject = Tools.WxTuneUp(prepay_id, WxPayConfig.appid);
            System.out.println("小程序所需参数封装完毕,二次加签完成!");
            r.setData(jsonObject);
            /**付款参数封装完毕*/
            UserDevice userDevice = new UserDevice();
            userDevice.setDeviceId(deviceId);
            userDevice.setUserId(userId);
            userDevice.setUseTime(useTime);
            /**
             * 此时应该在插入这条用户使用记录的地方设置redis缓存键值对,
             * key是用户id+设备id,value是时间。
             * */
            System.out.println("--------------插入缓存------------------");

            /**缓存记录该订单时间,到期缓存消失触发业务*/
            jedisUtil.InsertOrderServerListener(deviceId,userId,useTime);
            System.out.println("--------缓存成功,继续装箱--------");
            userDevice.setReceipt(receipt);
            userDevice.setOrderTime(orderTime);
            userDevice.setOrderState(true);
            System.out.println("-----------装箱完毕,校验成功后缓存记录------------");
            /**将该订单信息插入订单表,持久化到数据库*/
            boolean insertUserDevice = userDeviceService.InsertUserDevice(userDevice);
            /**将设备状态修改为已经被使用*/
            boolean updateDeviceState = deviceService.updateDeviceStateByDeviceId(deviceId);
            if (!insertUserDevice || !updateDeviceState) {
                throw new Exception("插入订单记录失败");
            }
            r.setCode(1);
            r.setMsg("下单成功");
            return r;
        } catch (Exception e) {
            e.printStackTrace();
            /**回滚支持*/
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return R.error("添加新的订单失败");
        }
    }

    /**
     * 删除未付款订单
     * @param userId 用户Id 9位
     * @param deviceId 设备Id 四位
     * @return
     */
    @RequestMapping("/deleteNoPayOrder")
    @ResponseBody
    public R deleteNoPayOrder(@RequestParam("userId")String userId,@RequestParam("deviceId")String deviceId){
        try{
            /**删除订单,删除缓存,其后释放设备加的锁*/
            boolean deleteNoPayOrder = userDeviceService.deleteNoPayOrder(userId);
            jedisUtil.DeleteOrderByDeviceId(deviceId);
            boolean freeDeviceByDeviceId = deviceService.freeDeviceByDeviceId(deviceId);
            if (deleteNoPayOrder && freeDeviceByDeviceId){
                return R.success("已删除未付款订单");
            }else
                throw new Exception("删除未付款订单失败");
        }catch (Exception e){
            System.out.println("删除未付款订单失败");
            return R.error("删除未付款订单失败");
        }

    } 
  

大家不难发现,这里的openid是直接由小程序端传过来的,就是上面我存在全局参数里的,当然当你没有支付的时候,确认订单状态在本次支付中没有改变,就默认你是没有支付,那么就会把这没支付的订单删除。

大家可能会好奇     String prepay_id = Tools.V3PayGet(order); 这一句才是核心呀!

不急在下面呢:

 public static String V3PayGet(String jsonStr) throws Exception {
        String body = "";
        //创建httpclient对象
        CloseableHttpClient client = HttpClients.createDefault();
        //创建post方式请求对象
        HttpPost httpPost = new HttpPost(url_prex + url);
        //装填参数
        StringEntity s = new StringEntity(jsonStr, charset);
        s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
                "application/json"));
        //设置参数到请求对象中
        httpPost.setEntity(s);
        String post = getToken(HttpUrl.parse(url_prex + url), jsonStr);
        //设置header信息
        //指定报文头【Content-type】、【User-Agent】
        httpPost.setHeader("Content-type", "application/json");
        httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
        httpPost.setHeader("Accept", "application/json");
        httpPost.setHeader("Authorization",
                "WECHATPAY2-SHA256-RSA2048 " + post);
        //执行请求操作,并拿到结果(同步阻塞)
        System.out.println("-------------执行统一下单请求--------------------------------");
        CloseableHttpResponse response = client.execute(httpPost);
        System.out.println("--------------请求统一下单获取预付单会话prepay_id--------------");
        //获取结果实体
        HttpEntity entity = response.getEntity();
        if (entity != null) {
            //按指定编码转换结果实体为String类型
            body = EntityUtils.toString(entity, charset);
            System.out.println(body);
        }
        EntityUtils.consume(entity);
        //释放链接
        response.close();
        System.out.println("-------------方法可走出?-------------");
        //返回JSAPI支付所需的参数
        String prepay_id = JSONObject.fromObject(body).getString("prepay_id");
        System.out.println("我觉得可以走出来!");
        return prepay_id;
    }

这里面为什么我在上面写的上二次加签验证呢?大家观察这一次的请求头的封装:

String post = getToken(HttpUrl.parse(url_prex + url), jsonStr);

我们看一下getToken()方法的代码:

    /**
     * 生成组装请求头
     *
     * @param url  请求地址
     * @param body 请求体
     * @return 组装请求的数据
     * @throws Exception 加密异常
     */
    static String getToken(HttpUrl url, String body) throws Exception {
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        long timestamp = System.currentTimeMillis() / 1000;
        String message = buildMessage(url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes(StandardCharsets.UTF_8));
        return "mchid=\"" + mchId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + mchSerialNo + "\","
                + "signature=\"" + signature + "\"";
    }

里面有sign()方法,就是第一次签名的加载啦:

    /**
     * 生成签名
     *
     * @param message 请求体
     * @return 生成base64位签名信息
     * @throws Exception 加密异常
     */
    static String sign(byte[] message) throws Exception {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey());
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }

(3)辅助工具类的设计与实现:

public class Tools {
    /**
     * 微信支付下单
     *
     * @param jsonStr 请求体 json字符串 此参数与微信官方文档一致
     * @return 订单支付的参数
     * @throws Exception 客户端处理异常
     */
    public static String V3PayGet(String jsonStr) throws Exception {
        String body = "";
        //创建httpclient对象
        CloseableHttpClient client = HttpClients.createDefault();
        //创建post方式请求对象
        HttpPost httpPost = new HttpPost(url_prex + url);
        //装填参数
        StringEntity s = new StringEntity(jsonStr, charset);
        s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
                "application/json"));
        //设置参数到请求对象中
        httpPost.setEntity(s);
        String post = getToken(HttpUrl.parse(url_prex + url), jsonStr);
        //设置header信息
        //指定报文头【Content-type】、【User-Agent】
        httpPost.setHeader("Content-type", "application/json");
        httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
        httpPost.setHeader("Accept", "application/json");
        httpPost.setHeader("Authorization",
                "WECHATPAY2-SHA256-RSA2048 " + post);
        //执行请求操作,并拿到结果(同步阻塞)
        System.out.println("-------------执行统一下单请求--------------------------------");
        CloseableHttpResponse response = client.execute(httpPost);
        System.out.println("--------------请求统一下单获取预付单会话prepay_id--------------");
        //获取结果实体
        HttpEntity entity = response.getEntity();
        if (entity != null) {
            //按指定编码转换结果实体为String类型
            body = EntityUtils.toString(entity, charset);
            System.out.println(body);
        }
        EntityUtils.consume(entity);
        //释放链接
        response.close();
        System.out.println("-------------方法可走出?-------------");
        //返回JSAPI支付所需的参数
        String prepay_id = JSONObject.fromObject(body).getString("prepay_id");
        System.out.println("我觉得可以走出来!");
        return prepay_id;
    }

    /**
     * 微信调起支付参数
     * 返回参数如有不理解 请访问微信官方文档
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
     *
     * @param prepayId 微信下单返回的prepay_id
     * @param appId    应用ID(appid)
     * @return 当前调起支付所需的参数
     * @throws Exception 加密异常
     */
    public static JSONObject WxTuneUp(String prepayId, String appId) throws Exception {
        String time = System.currentTimeMillis() / 1000 + "";
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        String packageStr = "prepay_id=" + prepayId;
        ArrayList list = new ArrayList<>();
        list.add(appId);
        list.add(time);
        list.add(nonceStr);
        list.add(packageStr);
        //加载签名
        System.out.println("----------小程序调起支付参数封装-----------");
        String packageSign = sign(buildSignMessage(list).getBytes());
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("appid", appId);
        jsonObject.put("timeStamp", time);
        jsonObject.put("nonceStr", nonceStr);
        jsonObject.put("packages", packageStr);
        jsonObject.put("signType", "RSA");
        jsonObject.put("paySign", packageSign);
        return jsonObject;
    }

    /**
     * 处理微信异步回调
     *
     * @param request    请求
     * @param response   响应
     * @param privateKey 32的秘钥
     */
    public static String notify(HttpServletRequest request, HttpServletResponse response, String privateKey) throws Exception {
        Map map = new HashMap<>(12);
        String result = readData(request);
        // 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
        String plainText = verifyNotify(result, privateKey);
        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();
        return JSONObject.fromObject(plainText).getString("out_trade_no");
    }

    /**
     * 生成组装请求头
     *
     * @param url  请求地址
     * @param body 请求体
     * @return 组装请求的数据
     * @throws Exception 加密异常
     */
    static String getToken(HttpUrl url, String body) throws Exception {
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        long timestamp = System.currentTimeMillis() / 1000;
        String message = buildMessage(url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes(StandardCharsets.UTF_8));
        return "mchid=\"" + mchId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + mchSerialNo + "\","
                + "signature=\"" + signature + "\"";
    }

    /**
     * 生成签名
     *
     * @param message 请求体
     * @return 生成base64位签名信息
     * @throws Exception 加密异常
     */
    static String sign(byte[] message) throws Exception {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey());
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }

    /**
     * 组装签名加载
     *
     * @param url       请求地址
     * @param timestamp 请求时间
     * @param nonceStr  请求随机字符串
     * @param body      请求体
     * @return 组装的字符串
     */
    static String buildMessage(HttpUrl url, long timestamp, String nonceStr, String body) {
        String canonicalUrl = url.encodedPath();
        if (url.encodedQuery() != null) {
            canonicalUrl += "?" + url.encodedQuery();
        }
        return "POST" + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }

    /**
     * 获取私钥。
     *
     * @return 私钥对象
     */

    static PrivateKey getPrivateKey() throws IOException {
        /*getPrivateKey静态定义的私钥路径,可放在数据库也可直接放在config文件存储为静态*/
//        String content = Files.readString(Paths.get(getPrivateKey));
        try {
            String privateKey = getPrivateKey.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");
            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(
                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    /**
     * 构造签名串
     *
     * @param signMessage 待签名的参数
     * @return 构造后带待签名串
     */
    static String buildSignMessage(ArrayList signMessage) {
        if (signMessage == null || signMessage.size() <= 0) {
            return null;
        }
        StringBuilder sbf = new StringBuilder();
        for (String str : signMessage) {
            sbf.append(str).append("\n");
        }
        return sbf.toString();
    }

    /**
     * v3 支付异步通知验证签名
     *
     * @param body 异步通知密文
     * @param key  api 密钥
     * @return 异步通知明文
     * @throws Exception 异常信息
     */
    static String verifyNotify(String body, String key) throws Exception {
        // 获取平台证书序列号
        cn.hutool.json.JSONObject resultObject = JSONUtil.parseObj(body);
        cn.hutool.json.JSONObject resource = resultObject.getJSONObject("resource");
        String cipherText = resource.getStr("ciphertext");
        String nonceStr = resource.getStr("nonce");
        String associatedData = resource.getStr("associated_data");
        AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8));
        // 密文解密
        return aesUtil.decryptToString(
                associatedData.getBytes(StandardCharsets.UTF_8),
                nonceStr.getBytes(StandardCharsets.UTF_8),
                cipherText
        );
    }

    /**
     * 处理返回对象
     *
     * @param request 请求
     * @return 返回对象内容
     */
    static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder result = new StringBuilder();
            br = request.getReader();
            for (String line; (line = br.readLine()) != null; ) {
                if (result.length() > 0) {
                    result.append("\n");
                }
                result.append(line);
            }
            return result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 获取随机字符串
     *
     * @return 随机字串
     */
    public static String generateNonceStr() {
        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
    }
}

这里对于辅助工具类不太了解的兄弟可以去看微信支付文档:

开发指引-小程序支付 | 微信支付商户平台文档中心 (qq.com)https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_2.shtml到这里我们微信支付开发中一个简单的案例就完成了,这是我实际自己研究的一套微信支付的设计,其中也有很多的优化点,但是实习期间我对自己的要求就是可以跑起来实现甲方的功能需求就可以,至于其他的毛病我们的项目达不到并发很高,所以我也没有系统的去优化,当然微信支付真的坑了我好久。

想看源码的可以评论下意图和邮箱

【来C站:一起聊java】用java实现微信支付功能的详细设计思路_第2张图片

 

你可能感兴趣的:(小程序学习,springboot,web学习,微信,java,spring,boot)