• java整合快手小程序(登陆,支付,结算,退款,手机号授权登陆)


    快手小程序官方文档

    快手小程序官网地址

    • 快手小程序后台配置回调域名

    代码部分

    • KSUrlConstants(请求地址常量)
    • 商品类目编号根据业务自行替换
    package com.dfjs.constant;
    
    /**
     * @author jigua
     * @version 1.0
     * @className KSUrlConstants
     * @description 快手接口请求地址
     * @create 2022/8/10 14:37
     */
    public class KSUrlConstants {
    
        /**
         * https://mp.kuaishou.com/docs/develop/server/code2Session.html
         * code获取openId sessionKey
         */
        public static final String CODE_2_SESSION = "https://open.kuaishou.com/oauth2/mp/code2session";
    
        /**
         * https://mp.kuaishou.com/docs/develop/server/getAccessToken.html
         * 接口调用凭证
         */
        public static final String GET_ACCESS_TOKEN = "https://open.kuaishou.com/oauth2/access_token";
    
        /**
         * https://mp.kuaishou.com/docs/develop/server/epay/interfaceDefinition.html
         * 预下单接口
         */
        public static final String CREATE_ORDER = "https://open.kuaishou.com/openapi/mp/developer/epay/create_order";
    
        /**
         * 退款
         * https://mp.kuaishou.com/docs/develop/server/epay/interfaceDefinition.html#_1-3%E6%94%AF%E4%BB%98%E5%9B%9E%E8%B0%83
         */
        public static final String APPLY_REFUND = "https://open.kuaishou.com/openapi/mp/developer/epay/apply_refund";
    
        /**
         * 结算
         * https://mp.kuaishou.com/docs/develop/server/epay/interfaceDefinition.html#_1-3%E6%94%AF%E4%BB%98%E5%9B%9E%E8%B0%83
         */
        public static final String APPLY_SETTLE = "https://open.kuaishou.com/openapi/mp/developer/epay/settle";
    
    
        /**
         * https://mp.kuaishou.com/docs/operate/platformAgreement/epayServiceCharge.html
         * 商品类目编号
         */
        public static final Integer PAY_TYPE = 3306;
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • RestTemplateUtil(rest发送请求工具类)
    import com.alibaba.fastjson.JSONObject;
    import com.dfjs.bean.BaseConfig;
    import com.dfjs.constant.KSUrlConstants;
    import com.dfjs.constant.TencentUrlConstants;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.stereotype.Service;
    import org.springframework.web.client.RestTemplate;
    
    import java.text.MessageFormat;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author jigua
     * @version 1.0
     * @className RestTemplateUtil
     * @description
     * @create 2022/3/28 15:49
     */
    @Service
    public class RestTemplateUtil {
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @Autowired
        private RestTemplate restTemplate;
    
        @Autowired
        private BaseConfig baseConfig;
     /**
         * 快手小程序post请求
         * code2session
         */
        public String ksPostRequestUrlencoded(JSONObject jsonObject, String url) {
            String result = "";
            try {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
                HttpEntity<String> formEntity = new HttpEntity<>(MessageFormat.format("js_code={0}&app_id={1}&&app_secret={2}", jsonObject.get("js_code"), jsonObject.get("appid"), jsonObject.get("secret")), headers);
                result = restTemplate.postForObject(url, formEntity, String.class);
            } catch (Exception e) {
                logger.error("快手小程序post请求异常{}", url);
                logger.error("post请求异常", e);
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 快手小程序获取accessToken
         */
        public String ksPostRequestUrlencoded() {
            String result = "";
            try {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
                HttpEntity<String> formEntity = new HttpEntity<>(MessageFormat.format("app_id={0}&app_secret={1}&&grant_type={2}", baseConfig.getKSAPPID(), baseConfig.getKSSECRET(), "client_credentials"), headers);
                result = restTemplate.postForObject(KSUrlConstants.GET_ACCESS_TOKEN, formEntity, String.class);
            } catch (Exception e) {
                logger.error("快手小程序post请求异常{}", KSUrlConstants.GET_ACCESS_TOKEN);
                logger.error("post请求异常", e);
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 快手
         * 支付 退款 结算
         */
        public String ksPostRequestJson(JSONObject jsonObject, String url, String appId, String accessToken) {
            String result = "";
            try {
                Map<String, String> map = new HashMap<>();
                map.put("app_id", appId);
                map.put("access_token", accessToken);
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
                HttpEntity<Object> formEntity = new HttpEntity<>(jsonObject, headers);
                result = restTemplate.postForObject(url + "/?app_id={app_id}&access_token={access_token}", formEntity, String.class, map);
            } catch (Exception e) {
                logger.error("快手小程序post请求异常{}", url);
                logger.error("post请求异常", e);
                e.printStackTrace();
            }
            return result;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • KsUtil
    package com.dfjs.util;
    
    import com.google.common.base.Joiner;
    import com.google.common.base.Strings;
    import org.apache.commons.codec.binary.Base64;
    import org.apache.commons.codec.digest.DigestUtils;
    import org.apache.commons.lang3.ObjectUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    import tk.mybatis.mapper.util.StringUtil;
    
    import javax.crypto.Cipher;
    import javax.crypto.spec.IvParameterSpec;
    import javax.crypto.spec.SecretKeySpec;
    import java.nio.charset.StandardCharsets;
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    import static javax.crypto.Cipher.DECRYPT_MODE;
    
    /**
     * @author jigua
     * @version 1.0
     * @className KSUtil
     * @description 快手签名工具类
     * @create 2022/8/10 14:35
     */
    @Component
    public class KSUtil {
    
        private final Logger LOGGER = LoggerFactory.getLogger(KSUtil.class);
    
        /**
         * 快手小程序返回的加密数据的解密函数
         *
         * @param sessionKey    有效的sessionKey,通过 login code 置换
         * @param encryptedData 返回的加密数据(base64编码)
         * @param iv            返回的加密IV(base64编码)
         * @return 返回解密的字符串数据
         */
        public String decrypt(String sessionKey, String encryptedData, String iv) {
            // Base64解码数据
            byte[] aesKey = Base64.decodeBase64(sessionKey);
            byte[] ivBytes = Base64.decodeBase64(iv);
            byte[] cipherBytes = Base64.decodeBase64(encryptedData);
    
            byte[] plainBytes = decrypt0(aesKey, ivBytes, cipherBytes);
    
            return new String(plainBytes, StandardCharsets.UTF_8);
        }
    
        /**
         * AES解密函数. 使用 AES/CBC/PKCS5Padding 模式
         *
         * @param aesKey      密钥,长度16
         * @param iv          偏移量,长度16
         * @param cipherBytes 密文信息
         * @return 明文
         */
        private byte[] decrypt0(byte[] aesKey, byte[] iv, byte[] cipherBytes) {
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec ivSpec = new IvParameterSpec(iv);
            try {
                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                cipher.init(DECRYPT_MODE, keySpec, ivSpec);
                return cipher.doFinal(cipherBytes);
            } catch (Exception e) {
                LOGGER.error("decrypt error.", e);
                throw new RuntimeException(e);
            }
        }
    
        /**
         * https://mp.kuaishou.com/docs/develop/server/epay/interfaceDefinition.html
         * 

    * https://mp.kuaishou.com/docs/develop/server/payment/serverSignature.html * 支付签名 */ public String buildMd5(Map<String, String> dataMap, String appSecret) { String signStr = genSignStr(dataMap); return DigestUtils.md5Hex(signStr + appSecret); } private String genSignStr(Map<String, String> data) { StringBuilder sb = new StringBuilder(); data.keySet().stream().sorted() .filter(key -> StringUtils.isNotBlank(key) && StringUtil.isNotEmpty(data.get(key))) .forEach(key -> { sb.append(key); sb.append("="); sb.append(data.get(key)); sb.append("&"); }); if (sb.length() > 0) { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); } /** * 获取参数 Map 的签名结果 * https://mp.kuaishou.com/docs/develop/server/epay/appendix.html * * @param signParamsMap 含义见上述示例 * @return 返回签名结果 */ public String calcSign(Map<String, Object> signParamsMap, String secret) { // 去掉 value 为空的 Map<String, Object> trimmedParamMap = signParamsMap.entrySet() .stream() .filter(item -> !Strings.isNullOrEmpty(item.getValue().toString())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // 按照字母排序 Map<String, Object> sortedParamMap = trimmedParamMap.entrySet() .stream() .sorted(Map.Entry.comparingByKey()) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new)); // 组装成待签名字符串。(注,引用了guava工具) String paramStr = Joiner.on("&").withKeyValueSeparator("=").join(sortedParamMap.entrySet()); String signStr = paramStr + secret; // 生成签名返回。(注,引用了commons-codec工具) return DigestUtils.md5Hex(signStr); } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137

    登陆

     @ApiOperation(value = "快手小程序code2Session", notes = "code:0-失败,1-成功")
        @ApiImplicitParam(name = "jsonObject", value = "code", required = true, dataType = "JSONObject")
        @PostMapping("/ks/ksLoginBind")
        @ResponseBody
        public String ksLoginBind(@RequestBody JSONObject jsonObject, HttpServletRequest request) {
     
    
            String code = jsonObject.getString("code");
            if (null == code) {
                return "code丢失";
            }
            JSONObject requestObject = new JSONObject();
            requestObject.put("appid", baseConfig.getKSAPPID());
            requestObject.put("secret", baseConfig.getKSSECRET());
            requestObject.put("js_code", code);
            String result = restTemplateUtil.ksPostRequestUrlencoded(requestObject, KSUrlConstants.CODE_2_SESSION);
            if (!"".equals(result)) {
                JSONObject resultObj = JSONObject.parseObject(result);
                logger.info("快手用户解析信息{}", resultObj);
    
                String resultCode = resultObj.getString("result");
                if (null != resultCode && "1".equals(resultCode)) {
                    String session_key = resultObj.getString("session_key");
                    String openid = resultObj.getString("open_id");
                    //处理业务逻辑
    
                } else {
                    return "参数错误[" + resultCode + "]";
                }
            } else {
               return "解析异常请重试";
            }
    
            return "";
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    手机号授权登陆

    @ApiOperation(value = "快手小程序手机号授权登陆", notes = "code:0-失败,1-成功")
        @ApiImplicitParam(name = "jsonObject", value = "用户实体", required = true, dataType = "JSONObject")
        @PostMapping("/ks/login")
        @ResponseBody
        public String ksUserLogin(@RequestBody JSONObject jsonObject) {
    
            String code = jsonObject.getString("code");
            if (null == code) {
                return "code丢失";
            }
            logger.info("快手授权请求参数{}", JSONObject.toJSONString(jsonObject));
            try {
                //通过code获取openid和session_key
                JSONObject requestObject = new JSONObject();
                requestObject.put("appid","小程序对应的appid");
                requestObject.put("secret", "小程序对应的secret");
                requestObject.put("js_code", code);
                String result = restTemplateUtil.ksPostRequestUrlencoded(requestObject, KSUrlConstants.CODE_2_SESSION);
                if (!"".equals(result)) {
                    JSONObject resultObj = JSONObject.parseObject(result);
                    String resultCode = resultObj.getString("result");
                    logger.info("快手用户解析信息{}", resultObj);
    
                    if (null != resultCode && "1".equals(resultCode)) {
                        String openid = resultObj.getString("open_id");
                        String session_key = resultObj.getString("session_key");
                        //解析手机号密文
                        String ksUserInfo = ksUtil.decrypt(session_key, jsonObject.getString("encryptedData"), jsonObject.getString("iv"));
                        logger.info("快手用户手机号{}", ksUserInfo);
                        if (!StringUtil.isEmpty(ksUserInfo)) {
                            JSONObject ksUserJson = JSONObject.parseObject(ksUserInfo);
                            String phoneNumber = ksUserJson.getString("phoneNumber");
                            if (!StringUtil.isEmpty(phoneNumber)) {
                            	//处理业务逻辑
                            	return phoneNumber;
                            } else {
                                return "未找到手机号";
                            }
                        } else {
                            return "手机号解析失败";
                        }
                    } else {
                        if(null == resultCode){
                            resultCode = resultObj.getString("err_no");
                        }
                       return "参数错误[" + resultCode + "]";
                    }
                } else {
                   return "code解析异常请重试";
                }
    
    
            } catch (Exception e) {
                logger.error("抖音授权登陆失败:{}", e);
               return "授权登陆失败";
            }
            return "";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    支付

    • 支付前需要先获取到用户的openId,用户openId参与支付签名
    • 支付前需要先获取到支付权限的access_token
    • access_token 在支付 结算 退款中都要用

    获取access_token示意代码

    • 此处仅为获取access_token的示例,大家根据个人编码风格自行使用
        public String getAccessToken(){
            String ks_pay_access_token = redisService.getData("ks_pay_access_token");
            if (null == ks_pay_access_token) {
                String result = restTemplateUtil.ksPostRequestUrlencoded();
                logger.info("快手小程序获取token结果{}", result);
                if (null != result) {
                    JSONObject object = JSONObject.parseObject(result);
                    logger.info("快手小程序获取token结果JSON{}", object);
    
                    String resultCode = object.getString("result");
                    if (null != resultCode && resultCode.equals("1")) {
                        ks_pay_access_token = object.getString("access_token");
                        //token 48小时有效
                        redisService.addData("ks_pay_access_token", ks_pay_access_token, 60 * 60 * 47, TimeUnit.SECONDS);
                    } else {
                        return "token获取失败[" + resultCode + "]";
                    }
                } 
            }
            
            return ks_pay_access_token;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    支付不区分微信还是支付宝,快手会在回调中通知使用了何种支付方式

    	@Override
        @Transactional(rollbackFor = Exception.class)
        public JSONObject ksAppletPay(String outTradeNo, String ks_pay_access_token, String openId) {
        
            try {
    
                //加签验签的参数需要排序
                Map<String, Object> params = new TreeMap<String, Object>();
    
                //小程序APPID
                params.put("app_id", "小程序的appid");
    
                //开发者侧的订单号。需保证同一小程序下不可重复
                params.put("out_order_no", outTradeNo);
    
                //快手用户在当前小程序的open_id,可通过login操作获取
                params.put("open_id", openId);
    
                //用户支付金额,单位为[分]。不允许传非整数的数值。
                params.put("total_amount", (new BigDecimal(100).multiply(new BigDecimal(100))).stripTrailingZeros().toPlainString());
                //商品描述。
                params.put("subject", "商品描述");
                //商品详情
                params.put("detail","商品详情");
                //商品类型,不同商品类目的编号见 担保支付商品类目编号
                params.put("type", KSUrlConstants.PAY_TYPE);
                //订单过期时间,单位秒,300s - 172800s
                params.put("expire_time", 1800);
    
                //开发者自定义字段,回调原样回传。超过最大长度会被截断
                params.put("attach", "支付测试");
    
    
                //通知地址
                params.put("notify_url", "支付回调通知地址");
    
                String sign = ksUtil.calcSign(params, "小程序的secret");
                params.put("sign", sign);
    
                JSONObject payJson = new JSONObject();
    
    
                payJson.put("out_order_no", outTradeNo);
                payJson.put("open_id", openId);
                payJson.put("total_amount", (new BigDecimal(100).multiply(new BigDecimal(100))).stripTrailingZeros().toPlainString());
                payJson.put("subject", "商品描述");
                payJson.put("detail", "商品详情");
                payJson.put("type", KSUrlConstants.PAY_TYPE);
                payJson.put("expire_time", 1800);
                payJson.put("sign", sign);
                payJson.put("attach", "测试支付");
                payJson.put("notify_url", baseConfig.getKSNOTIFYURL());
    
                logger.info("请求参数{}", payJson);
    
                //预下单接口
                String result = restTemplateUtil.ksPostRequestJson(payJson, KSUrlConstants.CREATE_ORDER, "小程序appid", ks_pay_access_token);
                logger.info("==================================");
                logger.info("快手预下单result{}", result);
                logger.info("==================================");
                if (!"".equals(result)) {
                    JSONObject jsonObject = JSONObject.parseObject(result);
                    String resultCode = jsonObject.getString("result");
                    if (null != resultCode && "1".equals(resultCode)) {
                        JSONObject data = jsonObject.getJSONObject("order_info");
                        String order_no = data.getString("order_no");
                        String order_info_token = data.getString("order_info_token");
                        if (null != order_info_token && null != order_no) {
    
                            //保存预下单信息
    						
    						//把order_no和order_info_token返回前端用于调起收银台
    						return data;
                        } else {
                            return null;
                        }
                    } else {
                    	// 参数错误[" + resultCode + "]
    					 return JSONObject.parseObject(resultCode);
                    }
                } else {
                    return JSONObject.parseObject("支付超时请重试");
                }
    
            } catch (Exception e) {
                e.printStackTrace();
               
                logger.error("快手小程序支付异常:{}", e);
            }
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    支付回调

    下面这一小段是重点↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

    • 这里有个坑,因为我使用的是阿里巴巴的fastjson包,会在接受参数时把null值给忽略掉,导致该json数据中获取不到对应的key

    • 举个栗子 ↓↓↓↓↓

    • “key”:null ,“key1”,“1”,“key2”:2 接收时只能object.getString(“key1”) || object.getString(“key2”) ,object.getString(“key”)不存在,但是在校验签名的时候key也是要参与校验的

    • 所以在拿到参数后可以使用 JSONObject.toJSONString(object, SerializerFeature.WRITE_MAP_NULL_FEATURES, SerializerFeature.QuoteFieldNames); 来保留null值的键值对

    • 也可以配置全局保留null值(请自行搜索解决方案)

    • 验签参数包含两部分,body一部分,header中一部分

    • header中获取到的kwaisign为快手返回的对本次请求的签名

    • 签名方式为取出http body中的原始字符串拼接app_secret,然后使用MD5进行签名:MD5(${http_body_string} + ${app_secret})

    • 然后二者进行比较即为校验成功

    /**
         * 快手支付结果通知
         *
         * @param
         * @return
         */
        @ApiOperation(value = "快手支付结果通知")
        @ResponseBody
        @RequestMapping("/ksPay/notify")
        public JSONObject ksPayNotify(@RequestBody JSONObject object, HttpServletRequest request) {
            logger.info("快手微信支付异步通知开始==============》{}", object);
            logger.info("快手微信支付kwaisign==============》{}", request.getHeader("kwaisign"));
    
            String jsonString = JSONObject.toJSONString(object, SerializerFeature.WRITE_MAP_NULL_FEATURES, SerializerFeature.QuoteFieldNames);
            logger.info("jsonString:" + jsonString);
    
            String kwaisign = request.getHeader("kwaisign");
            JSONObject returnObj = new JSONObject();
            returnObj.put("result", 0);
            returnObj.put("message_id", "business fail");
    
            if (null != kwaisign && null != jsonString) {
                jsonString = jsonString + "小程序的secret";
                //签名校验
                if (kwaisign.equals(DigestUtils.md5Hex(jsonString))) {
    
                    //当前回调消息的唯一ID,在同一个消息多次通知时,保持一致。
                    String message_id = object.getString("message_id");
    
                    JSONObject data = object.getJSONObject("data");
                    //支付渠道。取值:UNKNOWN - 未知|WECHAT-微信|ALIPAY-支付宝
                    String channel = data.getString("channel");
                    //订单支付状态。 取值: PROCESSING-处理中|SUCCESS-成功|FAILED-失败
                    String status = data.getString("status");
                    //快手小程序平台订单号
                    String ks_order_no = data.getString("ks_order_no");
                    //订单金额
                    String order_amount = data.getString("order_amount");
                    //用户侧支付页交易单号
                    String trade_no = data.getString("trade_no");
                    //商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
                    String out_order_no = data.getString("out_order_no");
    
                    //回调支付成功
                    if (null != status && "SUCCESS".equals(status)) {
                       //处理业务
                       
                       //正确处理后返回以下内容格式通知小程序平台不再持续回调
    				   returnObj.put("result", 1);
                       returnObj.put("message_id", message_id);
                   }
                } else {
                    logger.info("快手支付回调签名校验失败");
                }
            } else {
                logger.info("快手支付回调参数丢失");
            }
            logger.info("本次快手支付回调返回参数{}", returnObj);
            return returnObj;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 退款
        @Override
        @Transactional(rollbackFor = Exception.class)
        public String ksRefund(String out_order_no, String ks_pay_access_token, BigDecimal money) {
    
            try {
                //加签验签的参数需要排序
                Map<String, Object> params = new TreeMap<String, Object>();
    
                //小程序APPID
                params.put("app_id", "小程序appid");
    
                //开发者侧的订单号。需保证同一小程序下不可重复
                params.put("out_order_no", out_order_no);
    
                //开发者的退款单号
                String out_refund_no = UUID.randomUUID().toString().replace("-", "");
                params.put("out_refund_no", out_refund_no);
    
                //退款理由。1个字符=2个汉字
                String reason = "订单[" + out_order_no + "]退款或部分退款";
                params.put("reason", reason);
                //开发者自定义字段,回调原样回传。超过最大长度会被截断
                String attach = "平台退款";
                params.put("attach", attach);
                //通知地址
                params.put("notify_url", "退款回调地址");
    
                //用户支付金额,单位为[分]。不允许传非整数的数值。
                params.put("refund_amount", (money.multiply(new BigDecimal(100))).stripTrailingZeros().toPlainString());
    
                String sign = ksUtil.calcSign(params, "小程序secret");
                params.put("sign", sign);
    
                JSONObject refundJson = new JSONObject();
    
                refundJson.put("out_order_no", out_order_no);
                refundJson.put("out_refund_no", out_refund_no);
                refundJson.put("reason", reason);
                refundJson.put("attach", attach);
                refundJson.put("notify_url", "退款回调地址");
                refundJson.put("refund_amount", (money.multiply(new BigDecimal(100))).stripTrailingZeros().toPlainString());
                refundJson.put("sign", sign);
    
                logger.info("请求参数{}", refundJson);
    
                //退款
                String result = restTemplateUtil.ksPostRequestJson(refundJson, KSUrlConstants.APPLY_REFUND, "小程序appid", ks_pay_access_token);
                logger.info("==================================");
                logger.info("快手退款result{}", result);
                logger.info("==================================");
                if (!"".equals(result)) {
                    return ksUpdateRefundSettleCommon(result, BusinessConstants.TT_REFUND, out_order_no, out_refund_no, money);
                }
    
            } catch (Exception e) {
                e.printStackTrace();
                
                logger.error("快手小程序退款异常:{}", e);
            }
            return "处理失败";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • ** 因为懒得写了所以共用了快手和抖音的退款分账常量。。**

    • BusinessConstants常量
      在这里插入图片描述

    • 退款回调

      	@ApiOperation(value = "快手退款结果通知")
        @ResponseBody
        @RequestMapping("/ksPay/refundNotify")
        public JSONObject ksRefundNotify(@RequestBody JSONObject object, HttpServletRequest request) {
            logger.info("快手退款异步通知开始==============》{}", object);
            logger.info("快手退款kwaisign==============》{}", request.getHeader("kwaisign"));
    
            String jsonString = JSONObject.toJSONString(object, SerializerFeature.WRITE_MAP_NULL_FEATURES, SerializerFeature.QuoteFieldNames);
    
            logger.info("jsonString:" + jsonString);
    
            String kwaisign = request.getHeader("kwaisign");
            JSONObject returnObj = new JSONObject();
            returnObj.put("result", 0);
            returnObj.put("message_id", "business fail");
            if (null != jsonString && null != kwaisign) {
                jsonString = jsonString + baseConfig.getKSSECRET();
                if (kwaisign.equals(DigestUtils.md5Hex(jsonString))) {
                    JSONObject data = object.getJSONObject("data");
                    String status = data.getString("status");
                    if (null != status && WXPayConstants.SUCCESS.equals(status)) {
                        //快手小程序平台订单号。
                        String ks_order_no = data.getString("ks_order_no");
                        //开发者的退款单号
                        String out_refund_no = data.getString("out_refund_no");
    
                        String message_id = object.getString("message_id");
                        //处理业务逻辑
                        //返回通知
                        returnObj.put("result", 1);
                        returnObj.put("message_id", message_id);
                    }
                }
            }
    
            return returnObj;
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 结算
     	@Override
        @Transactional(rollbackFor = Exception.class)
        public String ksSettlement(String out_order_no, String ks_pay_access_token, BigDecimal money) {
    
    
            try {
                //加签验签的参数需要排序
                Map<String, Object> params = new TreeMap<String, Object>();
    
                //小程序APPID
                params.put("app_id", "appid");
    
                //开发者侧的订单号。需保证同一小程序下不可重复
                params.put("out_order_no", out_order_no);
    
                //开发者的退款单号
                String out_settle_no = UUID.randomUUID().toString().replace("-", "");
                params.put("out_settle_no", out_settle_no);
    
                //退款理由。1个字符=2个汉字
                String reason = "订单[" + out_order_no + "]结算";
                params.put("reason", reason);
                //开发者自定义字段,回调原样回传。超过最大长度会被截断
                String attach = "平台结算";
                params.put("attach", attach);
                //通知地址
                params.put("notify_url", "结算回调地址");
    
                String sign = ksUtil.calcSign(params,"secret");
                params.put("sign", sign);
    
                JSONObject refundJson = new JSONObject();
    
                refundJson.put("out_order_no", out_order_no);
                refundJson.put("out_settle_no", out_settle_no);
                refundJson.put("reason", reason);
                refundJson.put("attach", attach);
                refundJson.put("notify_url", "结算回调地址");
                refundJson.put("sign", sign);
    
                logger.info("请求参数{}", refundJson);
    
                //分账
                String result = restTemplateUtil.ksPostRequestJson(refundJson, KSUrlConstants.APPLY_SETTLE, "appid", ks_pay_access_token);
                logger.info("==================================");
                logger.info("快手结算result{}", result);
                logger.info("==================================");
                if (!"".equals(result)) {
                    return ksUpdateRefundSettleCommon(result, BusinessConstants.TT_SETTLE, out_order_no, out_settle_no, money);
                }
    
            } catch (Exception e) {
                e.printStackTrace();
                logger.error("快手小程序结算异常:{}", e);
            }
            return "处理失败";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    结算回调

     	@ApiOperation(value = "快手分账结果通知")
        @ResponseBody
        @RequestMapping("/ksPay/settleNotify")
        public JSONObject ksSettleNotify(@RequestBody JSONObject object, HttpServletRequest request) {
            logger.info("快手分账异步通知开始==============》{}", object);
            logger.info("快手分账kwaisign==============》{}", request.getHeader("kwaisign"));
    
            String jsonString = JSONObject.toJSONString(object, SerializerFeature.WRITE_MAP_NULL_FEATURES, SerializerFeature.QuoteFieldNames);
    
            logger.info("jsonString:" + jsonString);
    
            String kwaisign = request.getHeader("kwaisign");
            JSONObject returnObj = new JSONObject();
            returnObj.put("result", 0);
            returnObj.put("message_id", "business fail");
            if (null != jsonString && null != kwaisign) {
                jsonString = jsonString + baseConfig.getKSSECRET();
                if (kwaisign.equals(DigestUtils.md5Hex(jsonString))) {
                    JSONObject data = object.getJSONObject("data");
                    String status = data.getString("status");
                    if (null != status && WXPayConstants.SUCCESS.equals(status)) {
                        //快手小程序平台订单号。
                        String ks_order_no = data.getString("ks_order_no");
                        //快手小程序平台结算单号。
                        String ks_settle_no = data.getString("ks_settle_no");
    
                        //外部结算单号,即开发者结算请求的单号。
                        String out_settle_no = data.getString("out_settle_no");
    
                        String message_id = object.getString("message_id");
                        //处理业务逻辑
                        //返回通知
                        returnObj.put("result", 1);
                        returnObj.put("message_id", message_id);
    
                    }
                }
            }
    
            return returnObj;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • ksUpdateRefundSettleCommon
     /**
         * 快手退款或结算后修改订单信息
         * 0.处理失败
         * 1.处理成功
         */
        private String ksUpdateRefundSettleCommon(String result, Integer type, String outTradeNo, String settleRefundNo, BigDecimal money) {
    
            if (!"".equals(result)) {
                JSONObject jsonObject = JSONObject.parseObject(result);
                String resultCode = jsonObject.getString("result");
                if (null != resultCode && "1".equals(resultCode)) {
                    BusinessOrderKs businessOrderKs = businessOrderKsDao.getByOutTradeNo(outTradeNo);
                    if (null != businessOrderKs) {
                        //退款
                        if (type.equals(BusinessConstants.TT_REFUND)) {
    						String refund_no = jsonObject.getString("refund_no");
                        }
                        //结算
                        else if (type.equals(BusinessConstants.TT_SETTLE)) {
                            String settle_no = jsonObject.getString("settle_no");
                        }
    					
    					return "处理成功";
                    }
    
                } else {
                    String error_msg = jsonObject.getString("error_msg");
                    return resultCode + ":" + error_msg;
                }
    
            } else {
                return "值为空";
            }
    
            return "";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    写在最后的小坑

    • 如果正式服和测试服使用同一套appid和secret,如果和博主一样选择把快手拿到的token值存在redis中使用,则可能会导致旧的token在redis未过期,在快手处已经不可用(即先在测试服拿了一个token,立马又在正式服拿了一个token,那么此时测试服的token会不可用)
    • 快手在旧的token未过期时拿到新token,旧token在5分钟内仍然可用
    • 可以在代码中使用重试机制重新获取一次token来刷掉旧的不可用token来解决此问题(仅为个人建议)
    • 也可以直接在redis中删掉这个旧token,让代码直接重新拿(不建议,人工成本高)
  • 相关阅读:
    Linux_API_系列-整体概览
    QT收藏夹
    机器人过程自动化(RPA)入门 9. 管理和维护代码
    负号重载C++
    【web-攻击用户】(9.5)同源策略:与浏览器扩展、HTML5、通过代理服务应用程序跨域
    adb shell 指令集
    21天,胖哥亲自带你玩转OAuth2
    C# DeOldify 黑白照片 老照片上色
    SVG 绘制微信订阅号icon
    python之Scrapy爬虫案例:豆瓣
  • 原文地址:https://blog.csdn.net/zq6269/article/details/126505106