字节小程序官网地址
ByteDanceUrlConstants(代码请求地址常量)
package com.dfjs.constant;
/**
* @author jigua
* @version 1.0
* @className ByteDanceUrlConstants
* @description
* @create 2022/3/29 18:05
*/
public class ByteDanceUrlConstants {
/**
* 登陆
*/
public static final String CODE_2_SESSION = "https://developer.toutiao.com/api/apps/v2/jscode2session";
/**
* 生成预支付单
*/
public static final String CREATE_ORDER = "https://developer.toutiao.com/api/apps/ecpay/v1/create_order";
/**
* 分账
*/
public static final String SETTLE = "https://developer.toutiao.com/api/apps/ecpay/v1/settle";
/**
* 退款
*/
public static final String CREATE_REFUND = "https://developer.toutiao.com/api/apps/ecpay/v1/create_refund";
}
TTPayUtil(加签和验签工具类)
${rawData}${session_key}
)代码中的SALT和token从<准备工作图示位置>查找并替换正确的值
package com.dfjs.util;
import com.dfjs.bean.BaseConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.List;
/**
* @author jigua
* @version 1.0
* @className TTPayUtil
* @description 抖音支付签名工具类
* @create 2022/3/29 10:10
*/
@Component
public class TTPayUtil {
/**
* 发起请求时的签名
*/
public String getSign(Map<String, Object> paramsMap) {
List<String> paramsArr = new ArrayList<>();
for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
String key = entry.getKey();
if (key.equals("other_settle_params")) {
continue;
}
String value = entry.getValue().toString();
value = value.trim();
if (value.startsWith("\"") && value.endsWith("\"") && value.length() > 1) {
value = value.substring(1, value.length() - 1);
}
value = value.trim();
if (value.equals("") || value.equals("null")) {
continue;
}
switch (key) {
// 字段用于标识身份,不参与签名
case "app_id":
case "thirdparty_id":
case "sign":
break;
default:
paramsArr.add(value);
break;
}
}
// 支付密钥值
paramsArr.add("SALT秘钥串");
Collections.sort(paramsArr);
StringBuilder signStr = new StringBuilder();
String sep = "";
for (String s : paramsArr) {
signStr.append(sep).append(s);
sep = "&";
}
return md5FromStr(signStr.toString());
}
public String md5FromStr(String inStr) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
byte[] byteArray = inStr.getBytes(StandardCharsets.UTF_8);
byte[] md5Bytes = md5.digest(byteArray);
StringBuilder hexValue = new StringBuilder();
for (byte md5Byte : md5Bytes) {
int val = ((int) md5Byte) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
/**
* 回调验证签名
*/
public String getCallbackSignature(int timestamp, String nonce, String msg) {
List<String> sortedString = new ArrayList<>();
sortedString.add(String.valueOf(timestamp));
sortedString.add(nonce);
sortedString.add(msg);
sortedString.add("配置好的token串");
Collections.sort(sortedString);
StringBuilder sb = new StringBuilder();
sortedString.forEach(sb::append);
return getSha1(sb.toString().getBytes());
}
public String getSha1(byte[] input) {
MessageDigest mDigest;
try {
mDigest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
byte[] result = mDigest.digest(input);
StringBuilder sb = new StringBuilder();
for (byte b : result) {
sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
}
/**
* 手机号登陆信息解密
*/
public String decrypt(String encryptedData, String sessionKey, String iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Base64.Decoder decoder = Base64.getDecoder();
byte[] sessionKeyBytes = decoder.decode(sessionKey);
byte[] ivBytes = decoder.decode(iv);
byte[] encryptedBytes = decoder.decode(encryptedData);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec skeySpec = new SecretKeySpec(sessionKeyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivSpec);
byte[] ret = cipher.doFinal(encryptedBytes);
return new String(ret);
}
}
RestTemplateUtil(rest发送请求工具类)
package com.dfjs.util;
import clojure.lang.Obj;
import com.alibaba.fastjson.JSONObject;
import com.dfjs.bean.BaseConfig;
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.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;
/**
* 字节小程序post请求
*/
public String byteDancePostRequest(JSONObject jsonObject, String url) {
String result = "";
try {
HttpHeaders headers = new HttpHeaders();
//所有的请求需要用JSON格式发送
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
HttpEntity<Object> formEntity = new HttpEntity<>(jsonObject, headers);
result = restTemplate.postForObject(url, formEntity, String.class);
} catch (Exception e) {
logger.error("抖音小程序post请求异常{}", url);
e.printStackTrace();
}
return result;
}
}
@ApiOperation(value = "抖音小程序code2Session", notes = "code:0-失败,1-成功")
@ApiImplicitParam(name = "jsonObject", value = "code", required = true, dataType = "JSONObject")
@PostMapping("/tt/loginBind")
@ResponseBody
public String loginBind(@RequestBody JSONObject jsonObject, HttpServletRequest request) {
String code = jsonObject.getString("code");
if (null == code) {
return "code丢失";
}
JSONObject requestObject = new JSONObject();
requestObject.put("appid", "小程序对应的appid");
requestObject.put("secret", "小程序对应的secret");
requestObject.put("code", code);
requestObject.put("anonymous_code", "");
String result = restTemplateUtil.byteDancePostRequest(requestObject, ByteDanceUrlConstants.CODE_2_SESSION);
if (!"".equals(result)) {
JSONObject resultObj = JSONObject.parseObject(result);
String err_no = resultObj.getString("err_no");
if (null != err_no && "0".equals(err_no)) {
JSONObject jsonData = resultObj.getJSONObject("data");
String session_key = jsonData.getString("session_key");
String openid = jsonData.getString("openid");
String unionid = jsonData.getString("unionid");
//处理自己的业务逻辑
} else {
return "解析错误[" + err_no + "]";
}
} else {
return "解析异常请重试";
}
return "success";
}
Illegal base64 character 3a
javax.crypto.BadPaddingException: Given final block not properly padded
如果遇到了这两个错误,一般情况是参数错误导致(sessionKey 错误或者没有获取到手机号)
先调用tt.login()获取code
然后再调用getPhoneNumber获取iv和encryptedData
tt.login不能随意调用,必须在获取手机号按钮点击前调用, tt.login调用后会刷新登录态
getPhoneNumber中调用tt.login()会导致session_key失效
@PostMapping("/tt/login")
@ResponseBody
public String ttUserLogin(@RequestBody JSONObject jsonObject) {
String code = jsonObject.getString("code");
if (null == code) {
return "code丢失";
}
try {
//通过code获取openid和session_key
JSONObject requestObject = new JSONObject();
requestObject.put("appid", "app_id");
requestObject.put("secret", "secret");
requestObject.put("code", code);
requestObject.put("anonymous_code", "");
//调用code2session获取session_key
String result = restTemplateUtil.byteDancePostRequest(requestObject, ByteDanceUrlConstants.CODE_2_SESSION);
if (!"".equals(result)) {
JSONObject resultObj = JSONObject.parseObject(result);
String err_no = resultObj.getString("err_no");
if (null != err_no && "0".equals(err_no)) {
JSONObject jsonData = resultObj.getJSONObject("data");
String session_key = jsonData.getString("session_key");
String openid = jsonData.getString("openid");
String unionid = jsonData.getString("unionid");
//解析手机号密文
String ttUserInfo = ttPayUtil.decrypt(jsonObject.getString("encryptedData"), session_key, jsonObject.getString("iv"));
if (null != ttUserInfo) {
JSONObject ttUserJson = JSONObject.parseObject(ttUserInfo);
String phoneNumber = ttUserJson.getString("phoneNumber");
return phoneNumber ;
} else {
return "手机号解析失败";
}
} else {
return "参数错误[" + err_no + "]";
}
} else {
return "code解析异常请重试";
}
} catch (Exception e) {
return "exception";
}
return "fail";
}
@Override
public JSONObject ttAppletPay(String outTradeNo) {
JSONObject returnJson = new JSONObject();
try {
//加签验签的参数需要排序
Map<String, Object> params = new TreeMap<String, Object>();
//小程序APPID
params.put("app_id","app_id");
//开发者侧的订单号。需保证同一小程序下不可重复
params.put("out_order_no", outTradeNo);
//支付价格。单位为[分],取值范围:[1,10000000000] 100元 = 100*100 分
params.put("total_amount", (new BigDecimal("100").multiply(new BigDecimal(100))).stripTrailingZeros().toPlainString());
//商品描述。
params.put("subject", "商品描述");
//商品详情
params.put("body", "商品详情");
//订单过期时间(秒) 5min-2day
params.put("valid_time", 1800);
//开发者自定义字段,回调原样回传。超过最大长度会被截断
params.put("cp_extra", "xx平台充值");
//通知地址
params.put("notify_url", "回调通知地址");
//签名,详见https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/server/ecpay/TE
String sign = ttPayUtil.getSign(params);
params.put("sign", sign);
//以JSON格式拼好以下参数发送请求
JSONObject payJson = new JSONObject();
payJson.put("app_id", "app_id");
payJson.put("out_order_no", outTradeNo);
//此处需要传入一个数值类型,string会报错。。
payJson.put("total_amount", new BigDecimal((new BigDecimal("100").multiply(new BigDecimal(100))).stripTrailingZeros().toPlainString()));
payJson.put("subject","商品描述");
payJson.put("body", "商品详情");
payJson.put("valid_time", 1800);
payJson.put("sign", sign);
payJson.put("cp_extra", "xx平台充值");
payJson.put("notify_url","回调通知地址");
//预下单接口
String result = restTemplateUtil.byteDancePostRequest(payJson, ByteDanceUrlConstants.CREATE_ORDER);
if (!"".equals(result)) {
JSONObject jsonObject = JSONObject.parseObject(result);
String err_no = jsonObject.getString("err_no");
if (null != err_no && "0".equals(err_no)) {
JSONObject data = jsonObject.getJSONObject("data");
String order_id = data.getString("order_id");
String order_token = data.getString("order_token");
if (null != order_id && null != order_token) {
//前端使用此处返回的data来调起付款收银台
returnJson.put("pay_json",data)
} else {
returnJson.put("error_info","支付参数为空");
}
} else {
returnJson.put("error_info","参数错误[" + err_no + "]");
}
} else {
returnJson.put("error_info","支付订单创建失败");
}
} catch (Exception e) {
e.printStackTrace();
logger.error("抖音小程序微信支付异常:{}", e);
returnJson.put("error_info","抖音小程序微信支付异常");
}
return returnJson ;
}
小程序(前端)获取到order_id和order_token后,唤起收银台
tt.pay({
orderInfo: {
order_id: 6819903302604491021 ,
order_token:
CgsIARCABRgBIAQoARJOCkx+WgXqCUIwTel2V3siEGZ0++poigIM+SMMxtMx798Vj0ZYzoTYBqeNslodUC9X5KAOHkR1YbSBz6I6pXATh5faIGy7R72A9vwm0OczGgA= ,
},
service: 5,
success(res) {
if (res.code == 0) {
// 支付成功处理逻辑,只有res.code=0时,才表示支付成功
// 但是最终状态要以商户后端结果为准
}
},
fail(res) {
// 调起收银台失败处理逻辑
},
});
支付,分账,退款回调
/**
* 抖音微信支付回调
*
* @param
* @return
*/
@ApiOperation(value = "抖音通知")
@ResponseBody
@RequestMapping("/ttPayNotify")
public JSONObject ttPayNotify(@RequestBody JSONObject object, HttpServletRequest request) {
logger.info("抖音异步通知开始==============》");
boolean flag = false;
try {
//随机数
String nonce = object.getString("nonce");
//时间戳
Integer timestamp = object.getInteger("timestamp");
//签名
String msg_signature = object.getString("msg_signature");
//订单信息的json字符串
String message = object.getString("msg");
//校验回调签名
String signMessage = ttPayUtil.getCallbackSignature(timestamp, nonce, message);
if (signMessage.equals(msg_signature)) {
logger.info("签名校验成功======");
JSONObject msg = object.getJSONObject("msg");
//固定值SUCCESS
String status = msg.getString("status");
//抖音侧订单号
String order_id = msg.getString("order_id");
//这里无论回调失败还是成功,都需要都各个业务层去处理相关逻辑
if(null != status && "success".equals(status)){
// to do something
flag = true;
}else{
// to do something
}
} else {
logger.info("signMessage:" + signMessage);
logger.info("msg_signature:" + msg_signature);
logger.info("签名校验失败======");
}
} catch (Exception e) {
e.printStackTrace();
logger.error("抖音回调处理失败, 信息:" + e.getMessage());
}
JSONObject returnObj = new JSONObject();
if (flag) {
//成功处理后回复此固定值
returnObj.put("err_no", 0);
returnObj.put("err_tips", "success");
} else {
//失败回复此固定值
returnObj.put("err_no", 400);
returnObj.put("err_tips", "business fail");
}
return returnObj;
}
{
"msg":
"{ \"appid\":\"appId不能给你们看呀\",
\"cp_orderno\":\"开发者侧生成的支付订单号\",
\"cp_extra\":\"xx平台充值\",
\"way\":\"1\",
\"channel_no\":\"432090xxxxxxxx03299703257922\",
\"channel_gateway_no\":\"\",
\"payment_order_no\":\"PCP202203291551xxxxxxxxxxx83375\",
\"out_channel_order_no\":\"432090097xxxxxxxxxxx03257922\",
\"total_amount\":15,
\"status\":\"SUCCESS\",
\"seller_uid\":\"70781xxxxxxxxxx79810\",
\"extra\":\"\",
\"item_id\":\"\",
\"paid_at\":1648540275,
\"message\":\"\",
\"order_id\":\"708042xxxxxxxxxx623\"
}",
"msg_signature":"bd8488233935xxxxxxxxxx5301341844a38f5109",
"type":"payment",
"nonce":"6696",
"timestamp":"1648540275"
}
{
"timestamp": 1602507471,
"nonce": "797",
"msg": "{\"appid\":\"tt07e3715e98c9aac0\",
\"cp_orderno\":\"out_order_no_1\",
\"cp_extra\":\"\",
\"way\":\"2\",
\"payment_order_no\":\"2021070722001450071438803941\",
\"total_amount\":9980,
\"status\":\"SUCCESS\",
\"seller_uid\":\"69631798443938962290\",
\"extra\":\"null\",
\"item_id\":\"\"}",
"msg_signature": "52fff5f7a4bf4a921c2daf83c75cf0e716432c73",
"type": "payment"
}
@Override
public String ttRefund(String outTradeNo, String orderId, BigDecimal money) {
try {
//加签验签的参数需要排序
Map<String, Object> params = new TreeMap<String, Object>();
//小程序APPID
params.put("app_id", "app_id");
//商户退款单号
params.put("out_order_no", outTradeNo);
//商户分配退款号
params.put("out_refund_no", orderId);
//退款原因
params.put("reason", "订单[" + outTradeNo + "]退款或部分退款");
//退款金额,单位[分]
params.put("refund_amount", (money.multiply(new BigDecimal(100))).stripTrailingZeros().toPlainString());
//开发者自定义字段,回调原样回传。超过最大长度会被截断
params.put("cp_extra", "xx公司用户退款");
//通知地址
params.put("notify_url", "回调地址");
//签名,详见https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/server/ecpay/TE
String sign = ttPayUtil.getSign(params);
params.put("sign", sign);
JSONObject refundJson = new JSONObject();
refundJson.put("out_refund_no", orderId);
refundJson.put("out_order_no", outTradeNo);
//此处需要传入一个数值类型,string会报错。。
refundJson.put("reason", "订单[" + outTradeNo + "]退款或部分退款");
refundJson.put("notify_url", "回调地址");
refundJson.put("cp_extra", "xx公司用户退款");
refundJson.put("app_id", "app_id");
refundJson.put("sign", sign);
refundJson.put("refund_amount", new BigDecimal((money.multiply(new BigDecimal(100))).stripTrailingZeros().toPlainString()));
//退款
String result = restTemplateUtil.byteDancePostRequest(refundJson, ByteDanceUrlConstants.CREATE_REFUND);
if (!"".equals(result)) {
//退款和结算分账公共处理方法
return updateRefundSettleCommon(result, BusinessConstants.TT_REFUND, outTradeNo, orderId, money);
}
} catch (Exception e) {
e.printStackTrace();
logger.error("抖音小程序退款异常:{}", e);
}
return "fail";
}
{
"msg":
"{ \"appid\":\"appId真的不能给你们看呀\",
\"cp_refundno\":\"708xx551xxx436xxxx1\",
\"cp_extra\":\"xx公司用户退款\",
\"status\":\"SUCCESS\",
\"refund_amount\":15,
\"is_all_settled\":false,
\"refunded_at\":1648792155,
\"message\":\"\",
\"order_id\":\"708145xxxxxxxxxx761\",
\"refund_source\":0,
\"refund_no\":\"7081xxxxxxxxxx04009\"}",
"msg_signature":"b43f8a5c459106259xxxxxxxxxxb7bf856d13acc",
"type":"refund",
"nonce":"8347",
"timestamp":"1648792210"
}
{
"timestamp": 1602507471,
"nonce": "797",
"msg":
"{\"appid\":\"ttb8bece032785e300\",
\"cp_refundno\":\"RD818440313350422528011772773\",
\"cp_extra\":\"\",
\"status\":\"SUCCESS\",
\"refund_amount\":13800,
\"is_all_settled\":false,
\"refunded_at\":1645523993,
\"message\":\"\",
\"order_id\":\"7064214528778766632\"}",
"msg_signature": "52fff5f7a4bf4a921c2daf83c75cf0e716432c73",
"type": "refund"
}
抖音分账结算代码
{ "merchant_uid": "分账方商户号", "amount": 10 // 分账金额 }
@Override
public String ttSettlement(String outTradeNo,String orderId) {
try {
//加签验签的参数需要排序
Map<String, Object> params = new TreeMap<String, Object>();
//小程序APPID
params.put("app_id", "app_id");
//开发者侧的结算号, 不可重复
params.put("out_settle_no", orderId);
//商户分配订单号,标识进行结算的订单
params.put("out_order_no", outTradeNo);
//结算描述
params.put("settle_desc", "[" + outTradeNo + "]支付结算");
//其它分账方信息
params.put("settle_params", "");
//开发者自定义字段,回调原样回传。超过最大长度会被截断
params.put("cp_extra", "xx技术有限公司结算");
//通知地址
params.put("notify_url", "回调地址");
//签名,详见https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/server/ecpay/TE
String sign = ttPayUtil.getSign(params);
params.put("sign", sign);
JSONObject settleJson = new JSONObject();
settleJson.put("out_settle_no", orderId);
settleJson.put("out_order_no", outTradeNo);
settleJson.put("settle_desc", "[" + outTradeNo + "]支付结算");
settleJson.put("notify_url", "回调地址");
settleJson.put("cp_extra", "xx技术有限公司结算");
settleJson.put("app_id", "app_id");
settleJson.put("sign", sign);
settleJson.put("settle_params", "");
//分账
String result = restTemplateUtil.byteDancePostRequest(settleJson, ByteDanceUrlConstants.SETTLE);
if (!"".equals(result)) {
return updateRefundSettleCommon(result, BusinessConstants.TT_SETTLE, outTradeNo, null, null);
}
} catch (Exception e) {
e.printStackTrace();
logger.error("抖音小程序分账异常:{}", e);
}
return "fail";
}
{"msg":"
{\"appid\":\"....咳咳\",
\"cp_settle_no\":\"70804xxxxxxxx659527\",
\"cp_extra\":\"xx技术有限公司结算\",
\"status\":\"SUCCESS\",
\"rake\":0,
\"commission\":0,
\"settle_detail\":\"商户号70xxxx802xxxxx279810-分成金额(分)5\",
\"settled_at\":1649230587,
\"message\":\"SUCCESS\",
\"order_id\":\"7080xxxx735xxxx9527\",
\"channel_settle_id\":\"30000xxxxxxxxx40629076926264\",
\"settle_amount\":5,
\"settle_no\":\"708xxxxxxxxxxx898056\"}",
"msg_signature":"13af39373dxxxxxxxxxx94bb083227a343b74bdc",
"type":"settle",
"nonce":"7110",
"timestamp":"1649230588"}
{
"timestamp": 1602507471,
"nonce": "797",
"msg": "{\"appid\":\"tt07e3715e98c9aac0\",
\"cp_settle_no\":\"out_settle_no_1\",
\"cp_extra\":\"2856\",
\"status\":\"SUCCESS\",
\"rake\":95,\"commission\":0}",
"type": "settle",
"msg_signature": "b313c64257660defba884af0e83be4d79794b559"
}
updateRefundSettleCommon
private String updateRefundSettleCommon(String result, Integer type, String outTradeNo, String settleRefundNo, BigDecimal money) {
String result = "";
JSONObject jsonObject = JSONObject.parseObject(result);
String err_no = jsonObject.getString("err_no");
if (null != err_no && "0".equals(err_no)) {
//处理自己的业务逻辑
result = "success";
} else {
String err_tips = jsonObject.getString("err_tips");
result = err_no + ":" + err_tips;
}
return result;
}
UUID.randomUUID().toString().replace("-","")
懂的都懂。。