在通用交易系统中,支付流程如下
1、服务端-预下单:生成参数与签名信息(此过程不需要与抖音平台对接)
参考 生成下单参数与签名_抖音开放平台
2、小程序用户端:根据返回的参数与签名,拉起抖音支付(tt.requestOrder、tt.getOrderPayment)
参考 tt.requestOrder_抖音开放平台
3、服务端:接收抖音回调、或主动查询订单(此过程需要小程序正式发布)
1、生成应用公私钥
抖音的密钥分为:应用、平台两种类型,注意区分。
进入抖音签名工具平台
生成好后,需要将公钥配置到抖音的控制台中。
每次修改应用公钥,都会变更keyVersion 值,后续签名过程需要匹配该值。注意
2、生成参数与签名
注意查看 官网文档中对 [生成下单参数与签名] 必传参数的要求。
抖音非必传参数的两种要求:
1)创建对象中,不允许出现该参数
2)该参数需要给定默认值,不能为 null
待签名对象
import java.io.Serializable;
import java.util.List;/**** 抖音下单,签名对象* @author xuancg* @date 2025/6/6*/
public class DyOrderSignData implements Serializable {/**目前只支持传入一项*/private List<Sku> skuList;private String outOrderNo;/*** 订单总金额* 单位:分*/private Integer totalAmount;/**支付超时时间 秒、非必传*/private Integer payExpireSeconds = 300;private String payNotifyUrl;/**开发者自定义收款商户号、非必传*///private String merchantUid;private GoodsPage orderEntrySchema;/*** 屏蔽的支付方式,当开发者没有进件某个支付渠道,可在下单时屏蔽对应的支付方式。* 如:[1, 2]表示屏蔽微信和支付宝*/private Integer[] limitPayWayList = new Integer[0];public static class Sku implements Serializable{private String skuId;private Integer price;private Integer quantity;/**商品标题,长度 <= 256字节 */private String title;private List<String> imageList;/**商品类型,详见此处的商品类型枚举值*/private Integer type;/**交易规则标签组*/private String tagGroupId;/**非必传*///private IndustryOrderGoodsPage entrySchema;/**非必传*/private String skuAttr;}/*** 商品详情页*/public static class GoodsPage {/*** 订单详情页跳转路径,没有前导的“/”,长度 <= 512byte*/private String path;/*** 订单详情页路径参数,自定义的 json 结构,* 序列化成字符串存入该字段,平台不限制* 但是写入的内容需要能够保证生成访问订单详情的schema能正确跳转到小程序内部的订单详情页* 长度 <= 512byte* 选填*/// private String params;}}
签名工具类
public class DecryptUtils {public static String getByteAuthorization(String privateKeyStr, String data, String appId, String nonceStr, long timestamp, String keyVersion) {String byteAuthorization = "";try {// 生成签名String signature = getSignature(privateKeyStr, "POST", "/requestOrder", timestamp, nonceStr, data);System.out.println(signature);// 构造byteAuthorizationStringBuilder sb = new StringBuilder();sb.append("SHA256-RSA2048 ").append("appid=").append(appId).append(",").append("nonce_str=").append(nonceStr).append(",").append("timestamp=").append(timestamp).append(",").append("key_version=").append(keyVersion).append(",").append("signature=").append(signature).append("");byteAuthorization = sb.toString();} catch (Exception ex) {ex.printStackTrace();return "";}return byteAuthorization;}public static String getSignature(String privateKeyStr, String method, String uri, long timestamp, String nonce, String data) throws Exception {data = data.replace("\\", "");System.out.println(data);String rawStr = method + "\n" +uri + "\n" +timestamp + "\n" +nonce + "\n" +data + "\n";privateKeyStr = privateKeyStr.replace("\n","");System.out.println(rawStr);System.out.println(privateKeyStr);Signature sign = Signature.getInstance("SHA256withRSA");sign.initSign(string2PrivateKey(privateKeyStr));sign.update(rawStr.getBytes(StandardCharsets.UTF_8));return Base64.getEncoder().encodeToString(sign.sign());}public static PrivateKey string2PrivateKey(String privateKeyStr) {PrivateKey prvKey = null;try {byte[] privateBytes = Base64.getDecoder().decode(privateKeyStr);PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes);KeyFactory keyFactory = KeyFactory.getInstance("RSA");prvKey = keyFactory.generatePrivate(keySpec);} catch (Exception ex) {ex.printStackTrace();}return prvKey;}}
调用签名方法
/*** 待签名串一共有五行(且必须严格按照以下顺序),每一行必须以 \n(换行符,ASCII 编码值为 0x0A)结束。* POST\n/requestOrder\n请求时间戳\n请求随机串\ndata\n*/public String createByteSign(DyOrderSignData request){// 请求时间戳long timestamp = System.currentTimeMillis()/1000L;// 随机字符串String nonceStr = UUID.randomUUID().toString();String data = JSONUtil.toJsonStr(request);// 应用公钥版本,每次重新上传公钥后需要更新,可通过「开发管理-开发设置-密钥设置」处获取String keyVersion = "3";String appId = "";String appPrivateContent = "";return DecryptUtils.getByteAuthorization(appPrivateContent,data, appId, nonceStr, timestamp, keyVersion);}
获取应用私钥内容
/***
* classpath:app/rsa_private_key.pem
*/
public String loadKeyContent(String configPath) {String path = configPath;if (configPath.startsWith("classpath:")) {path = configPath.replaceFirst("classpath:", "");}if (!path.startsWith("/")) {path = "/" + path;}try {ClassPathResource classPathResource = new ClassPathResource(path);String content = FileUtils.readFileToString(classPathResource.getFile(), StandardCharsets.UTF_8);String[] repls = {"-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----", "\n", "\r"};for (String repl : repls) {content = content.replace(repl, "");}return content;} catch (Exception e) {e.printStackTrace();}return null;}
返回前端内容
DyOrderSignData request
1、签名内容:String byteSign = createByteSign(request);
2、原始对象字符串(不建议传原始对象,官网的示例不合理):String objStr = JSONUtil.toJsonStr(request);
3、问题问题
先通过抖音签名工具,验证生成的签名内容是否一致。
签名不一致
1、检查是否使用应用私钥进行签名
2、私钥字符串内容不能用换行符、头部、尾部申明、其他隐藏符号等。
3、转化的参数对象是否一致。即在第五行,传入的JSON对象,可以在代码中打印内容。然后粘贴到签名工具中进行验证。
前端签名失败
1、如果是参数缺少、格式错误:请添加参数默认值,或者调整参数格式
2、下单errNo:11084 下单errMsg:requestOrder:fail 签名校验异常
由后端返回下单的参数对象 字符串,不能由前端将对象进行json转化。
如果在上一步签名不一致问题解决后,仍然出现签名校验异常
1)检查控制台的应用公钥与私钥是否匹配
2)检查keyVersion版本是否匹配
3)检查后端的下单对象封装JSON格式与返回前端字符串的JSON格式是否一致。
4)JSON对象中,不能有转义符\\ 、换行符 等特殊符号
没有收到抖音回调
小程序没有发布上线