java整合字节小程序(登陆,微信支付,分账,退款,手机号授权)

字节小程序官方文档

字节小程序官网地址

1.准备工作

  1. 获取SALT秘钥,设置token令牌(不超过位数随便写),填写url回调地址
  2. 经测试,如果在代码中设置了回调地址,此处配置的url不生效
  3. token用作验签,秘钥用作加签

在这里插入图片描述

2.结算及分账注意事项

  1. 到店服务类订单(用户需要到店履约或到店核销):履约或核销后 3 天才可调用该接口发起结算(核销状态需要通过订单信息同步接口传入);
  2. 其他订单:支付成功 7 天后才可调用该接口发起结算。

3.退款注意事项

  1. 距离支付成功超 12 个月的订单不能退款,需要商户线下自行给用户退;
  2. 退款逻辑按照订单维度退,结算前发起退款,直接从「在途账户」退,结算后从「可提现账户」退。
  3. 结算前或结算后均支持退款,且支持全额退或部分退款。

4.代码部分

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(加签和验签工具类)

  • 回调签名算法
    1.将所有字段(验证时注意不包含 sign 签名本身,不包含空字段与 type 常量字段)内容与平台上配置的 token 一起,按照字典序排序
    2.所有字段内容连接成一个字符串
    3.使用 sha-1 算法计算字符串摘要作为签名
  • 请求签名算法
    1.sign, app_id , thirdparty_id 字段用于标识身份字段,不参与签名。将其他字段内容(不包含 key)与支付 SALT 一起进行字典序排序后,使用&符号链接
    2.使用 md5 算法对该字符串计算摘要,作为结果
    3.参与加签的字段均以 POST 请求中的 body 内容为准, 不考虑参数默认值等规则. 对于对象类型与数组类型的参数, 使用 POST 中的字符串原串进行左右去除空格后进行加签
    4.如有其他安全性需要, 可以在请求中添加 nonce 字段, 该字段无任何业务影响, 仅影响加签内容, 使同一请求的多次签名不同.
  • 手机号登陆信息解密算法
    1.signature = sha1(${rawData}${session_key})
    2.对称解密使用的算法为AES-128-CBC,数据采用PKCS#7填充。
    3.对称解密的目标密文为encryptedData。
    4.对称解密秘钥aeskey = Base64_Decode(session_key), aeskey长度为 16Byte。
    5.对称解密算法初始向量为Base64_Decode(iv)。

代码中的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发送请求工具类)

  • 支付,结算分账,提现统一使用post请求
  • JSON格式发送请求数据
  • 返回值是一个JSON字符串
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;
    }
}

登陆

  • 标识同一个用户应该使用mobile
  • 返回的openid,unionid同一个手机号在不同端登陆时值不同(大家可以自行测试。。)
 	@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";
    }

手机号授权登陆

  • 入参
  1. code
  2. iv
  3. encryptedData
  • 常见错误
  1. Illegal base64 character 3a

  2. javax.crypto.BadPaddingException: Given final block not properly padded

    如果遇到了这两个错误,一般情况是参数错误导致(sessionKey 错误或者没有获取到手机号)

  • 前端
  1. 先调用tt.login()获取code

  2. 然后再调用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;
    }
  • 测试支付回调值
  • order_id要存起来,用于之后的退款或者结算分账
{
	"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"
}

抖音退款

  • outTradeNo 开发者侧支付时生成的订单号
  • orderId 上面支付那块儿保存的order_id(自己定义一个新的也行。。)
  • money 需要退款的额度,支持全退和部分退

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

抖音分账结算代码

  • 其它分账方信息
  1. 其他分账方信息,分账分配参数 SettleParameter 数组序列化后生成的 json 格式字符串
  2. 只需要传入卖家之外的分账方
  3. { "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;
    }

注:退款和分账传入的orderId可以直接使用最开始抖音侧分配的单号,也可以自己定义

UUID.randomUUID().toString().replace("-","") 



懂的都懂。。
  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
Java 后端可以使用 HttpServletResponse 的 OutputStream 来返回 PDF 文件给微信小程序。具体实现步骤如下: 1. 在后端,使用 PDFBox 等工具生成 PDF 文件。 2. 将 PDF 文件保存到服务器上,获取文件的路径或者字节数组。 3. 在返回响应之前,设置 response 的 content type 为 "application/pdf"。 4. 使用 response.getOutputStream() 获取字节输出流。 5. 将 PDF 文件的字节数组或者输入流写入到输出流中。 6. 关闭输出流。 示例代码: ```java @RequestMapping("/downloadPDF") public void downloadPDF(HttpServletResponse response) throws IOException { // 生成 PDF 文件 PDDocument doc = new PDDocument(); PDPage page = new PDPage(); doc.addPage(page); PDFont font = PDType1Font.TIMES_ROMAN; PDPageContentStream contentStream = new PDPageContentStream(doc, page); contentStream.beginText(); contentStream.setFont(font, 12); contentStream.newLineAtOffset(100, 700); contentStream.showText("Hello, World!"); contentStream.endText(); contentStream.close(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); doc.save(baos); doc.close(); byte[] pdfBytes = baos.toByteArray(); // 返回响应 response.setContentType("application/pdf"); response.setHeader("Content-Disposition", "attachment; filename=\"test.pdf\""); response.setContentLength(pdfBytes.length); ServletOutputStream sos = response.getOutputStream(); sos.write(pdfBytes); sos.flush(); sos.close(); } ``` 注意:以上示例代码仅供参考,实际代码需要根据具体情况进行调整和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值