IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> java整合字节小程序(登陆,微信支付,分账,退款,手机号授权) -> 正文阅读

[移动开发]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 字段, 该字段无任何业务影响, 仅影响加签内容, 使同一请求的多次签名不同.

代码中的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();
    }
}

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

抖音调用微信支付代码

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

抖音分账结算代码

基本同退款
  • 测试分账结算回调值
没找到沙盒测试方法,分账需要支付后7天才能执行,代码和测试数据完事儿再补
  • 官方文档分账结算回调值
{
  "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;
    }

手机号授权还没有从抖音申请下来权限。。。等写完再补代码

附:vue管理端处理退款分账 设计思路

  • 同一条成功支付单可进行一次全额(部分)退款和一次分账
  • 当同一条数据执行过退款和分账后,此条数据不可再操作
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-04-04 12:22:54  更:2022-04-04 12:25:29 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 20:36:32-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码