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

这篇具有很好参考价值的文章主要介绍了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;

}

  • 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;
    }

}
  • 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
     * <p>
     * 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);
    }
}

登陆

 @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 "";
    }

手机号授权登陆

@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 "";
    }

支付

  • 支付前需要先获取到用户的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;
    }

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

	@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;
    }

支付回调

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

  • 这里有个坑,因为我使用的是阿里巴巴的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;
    }
  • 退款
    @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 "处理失败";
    }
  • ** 因为懒得写了所以共用了快手和抖音的退款分账常量。。**

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

  • 退款回调

  	@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;
    }

  • 结算
 	@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 "处理失败";
    }

结算回调文章来源地址https://www.toymoban.com/news/detail-500218.html

 	@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;
    }
  • 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 "";
    }

写在最后的小坑

  • 如果正式服和测试服使用同一套appid和secret,如果和博主一样选择把快手拿到的token值存在redis中使用,则可能会导致旧的token在redis未过期,在快手处已经不可用(即先在测试服拿了一个token,立马又在正式服拿了一个token,那么此时测试服的token会不可用)
  • 快手在旧的token未过期时拿到新token,旧token在5分钟内仍然可用
  • 可以在代码中使用重试机制重新获取一次token来刷掉旧的不可用token来解决此问题(仅为个人建议)
  • 也可以直接在redis中删掉这个旧token,让代码直接重新拿(不建议,人工成本高)

到了这里,关于java整合快手小程序(登陆,支付,结算,退款,手机号授权登陆)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 微信小程序的登陆界面怎么写(手机号登陆)

    微信小程序的登录界面可以通过使用 view 、 input 、 button 等组件来实现。以下是一个简单的登录界面示例代码: 其中, .login-container 是登录页面的容器, .login-title 是登录标题, .form-item 是表单项容器, input 是输入框, .login-btn 是登录按钮。

    2024年02月16日
    浏览(47)
  • JAVA支付宝小程序授权登陆,并生成二维码(证书方式)

    接入准备 https://opendocs.alipay.com/open/284/106001?ref=api 根据文档进行创建小程序、配置相关信息(接口加签方式选择证书) 生成二维码文档https://opendocs.alipay.com/mini/02owto 开发接口

    2024年02月11日
    浏览(48)
  • 【微信支付】springboot-java接入微信支付-JSAPI支付/查单/退款/发送红包(二)---查单

    文章地址:https://blog.csdn.net/ssdadasd15623/article/details/134684556 查询订单分为微信订单号查询以及商户订单号查询,这里使用商户订单号,也就是自己的系统的订单号 https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/query-by-out-trade-no.html 在请求接口时,注意⚠️:请求参数内的Authori

    2024年02月03日
    浏览(43)
  • WxJava开发小程序登录、手机号、小程序码、微信支付

    WxJava是微信Java开发工具包,支持包括微信支付、开放平台、公众号、企业微信/企业号、小程序等微信功能模块的后端开发。 名称 网站 Gitee官网 https://gitee.com/binary/weixin-java-tools WxJava在线文档 http://wxjava.fly2you.cn/zh-CN/start/pay/explain.html 开发文档Wiki https://github.com/wechat-group/WxJa

    2024年02月05日
    浏览(40)
  • 支付宝小程序:获取用户信息和手机号快捷登录

    随着支付宝小程序以其便捷性和安全性受到了广大开发者和用户的青睐。在uni-app框架下开发支付宝小程序时,如何安全、合规地获取用户信息和手机号成为了开发者必须面对的问题。 1.在开始之前,确保你已经在支付宝开放平台上注册并创建应用,获取到应用的AppID; 2.小程

    2024年04月28日
    浏览(46)
  • uniapp - [最新] 超详细实现支付宝小程序获取手机号、个人信息,支付宝弹框授权拿到用户手机号与昵称头像等信息完整流程(详细示例源码与注释,一键复制快速应用到项目中)

    网上的文章乱七八糟的都不行,新手小白很难看懂,本篇文章通俗易懂。 本博客实现了在uniapp 支付宝小程序项目中,从 0-1 描述了如何授权获取用户的手机号和头像昵称信息,一看就能懂 你可以跟着教程一步步走,从支付宝后台配置再到详细的示例,稍微改改参数就能快速

    2024年02月06日
    浏览(118)
  • SpringBoot整合微信小程序登录获取手机号并解密

    SpringBoot+微信小程序  文章目录: 一、小程序登录获取手机号的流程 二、pom导入所需的依赖包 三、接收微信小程序的参数 四、后端发请求的util工具方法 五、获取手机号的接口 六、请求接口获取的phone_info信息 1.前端请求getPhoneNumber方法获取code传给后端接口; 2.后端接口通过

    2024年02月09日
    浏览(59)
  • UniApp + SpringBoot 实现接入支付宝支付功能和退款功能

    注册支付宝支付功能需要个体工商户或企业才可以!需要有营业执照才能去申请哦! 进入支付宝开放平台 控制台 如果没有绑定APP支付就会报商家订单参数异常,请重新发起支付的错误 AlipayConfig类代码 BizAlipayService类代码 接口代码 代码 返回结果: 返回结果里面的trade_no 一会

    2023年04月09日
    浏览(34)
  • 苹果(apple)支付退款通知、api

    背景: 用户在使用苹果支付购买商品后,可以直接像苹果申请退款,如果申请成功将导致商户直接构成损失。甚至某网络平台有这种专门薅羊毛的店铺,低价出售虚拟商品,再申请退款。所以有必要对用户发起的退款订单做及时响应,比如扣除对应的虚拟商品或像apple官方提

    2024年02月07日
    浏览(40)
  • Vue3电商项目实战-结算&支付 3【05-结算-收货地址-添加、06-结算-收货地址-修改、07-结算-提交订单】

    目的:实现收货地址的添加。 大致步骤: 独立组件,准备一个对话框 完成表单布局 完成确认添加操作 落的代码: 1.独立组件,准备一个对话框 src/views/member/pay/components/address-edit.vue 添加地址组件 src/views/member/pay/components/checkout-address.vue 使用添加地址组件 2.完成表单布局 s

    2023年04月08日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包