快手小程序官方文档
快手小程序官网地址
- 快手小程序后台配置回调域名
代码部分
- 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常量
-
退款回调文章来源:https://www.toymoban.com/news/detail-500218.html
@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模板网!