需要注意的是抖音支付属于聚合支付,在抖音小程序支付无需自己写微信、支付宝相关代码,就统一用抖音支付就行,本文的一些PHP代码仅供前端参考使用,php专业就算了 - -
前期准备
你可能需要给小程序先开通支付功能:小程序控制台-支付,并在设置内完成一些基础设置,包括小程序类目等
设定密钥(签名与验签会用到):小程序控制台-开发-开发配置-生成密钥
配置支付结果回调地址:小程序控制台-开发-解决方案管理(借用一张其他博主的图,名称见水印,见下图)
生成订单参数时没有回调地址参数,所以需要在这里设置,推荐连着退款通知回调地址(虽然退款回调地址在发起退款时可以有参数设置)一块设置了,设置完成要点击“发布上线”
编译好的代码要在根目录新建package.json文件,每次都要新建,还要重启编译器。官方文档-使用限制都有,内容为,
{
"industrySDK": true
}
其他废话不多说,直接上代码:
JS部分:JS api官方文档
//request是我自己封装的发送请求函数,可用自己函数或uni.request也行
// #ifdef MP-TOUTIAO
request({
url: `xxx.php`, //请求data及byteAuthorization的地址
data: {
"xxxxx": "xxxxxxxxxx" //传一些订单参数
},
method: 'POST',
needLoading: true
}).then(v => {
tt.requestOrder({
data:v.data,
byteAuthorization:v.byteAuthorization,
success: function(res) {
tt.getOrderPayment({
orderId: res.orderId,
success: function(res) {
//支付成功逻辑
},
fail: function(res) {
//支付失败逻辑
}
})
},
fail: function(res) {
//支付失败逻辑
}
})
}).catch(e => {
console.log(e)
//发送请求失败或获取结果失败等等
})
// #endif
PHP部分,xxx.php 生成下单参数与签名-官方文档
<?php
date_default_timezone_set('prc');
/*
$price = $_POST['price'];
$pay_type = $_POST['pay_type'];
$order_id = $_POST['order_id'];
....一些下单参数xxx
*/
if(!$price || !$pay_type){
echo json_encode(array("resultCode"=>800,"message"=>"价格或支付方式不能为空"));
exit;
}
$root = $_SERVER['DOCUMENT_ROOT'];
function getByteAuthorization($privateKeyStr, $data, $appId, $nonceStr, $timestamp, $keyVersion) {
$byteAuthorization = '';
// 读取私钥
$privateKey = openssl_pkey_get_private($privateKeyStr);
if (!$privateKey) {
throw new InvalidArgumentException("Invalid private key");
}
// 生成签名
$signature = getSignature("POST", "/requestOrder", $timestamp, $nonceStr, $data, $privateKey);
if ($signature === false) {
return null;
}
// 构造 byteAuthorization
$byteAuthorization = sprintf("SHA256-RSA2048 appid=%s,nonce_str=%s,timestamp=%s,key_version=%s,signature=%s", $appId, $nonceStr, $timestamp, $keyVersion, $signature);
return $byteAuthorization;
}
function getSignature($method, $url, $timestamp, $nonce, $data, $privateKey) {
// printf("method:%s\n url:%s\n timestamp:%s\n nonce:%s\n data:%s", $method, $url, $timestamp, $nonce, $data);
$targetStr = $method. "\n" . $url. "\n" . $timestamp. "\n" . $nonce. "\n" . $data. "\n";
openssl_sign($targetStr, $sign, $privateKey, OPENSSL_ALGO_SHA256);
$sign = base64_encode($sign);
return $sign;
}
function randStr($length = 8) {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$str = '';
for ($i = 0; $i < $length; $i++) {
$str .= $chars[mt_rand(0, strlen($chars) - 1)];
}
return $str;
}
// 请求时间戳
$timestamp = time();
// 随机字符串
$nonceStr = randStr(10);
// 应用公钥版本,每次重新上传公钥后需要更新,可通过「开发管理-开发设置-密钥设置」处获取
$keyVersion = "1";
// 应用私钥,用于加签 重要:1.测试时请修改为开发者自行生成的私钥;2.请勿将示例密钥用于生产环境;3.建议开发者不要将私钥文本写在代码中
//定义私钥证书
$private_key_file = $root . "xxxx/private_key.pem"; //选择私钥位置
$privateKeyStr = openssl_get_privatekey(file_get_contents($private_key_file));
$totalAmount = $price * 100; //注意金额单位为分,所以这里*100了
$limitPayWayList = '[1]'; //屏蔽的支付方式,我这里不区分订单来源了,屏蔽了微信支付
//这里可以做一些生成订单号,连接数据库等操作
if($pay_type === 'xxx1'){
$skuId = 0; //我这个固定填的0
$title = 'xxx';
$trade_no = "xxxx";
$quantity = 1;//数量
$typeId = 'xxx';
$tagGroupId = 'tag_group_xxxxxxxx'; //文档描述的“不可退款”是面对用户的,即用户不能在抖音的订单中心退款,商家仍可以通过调用“发起退款”接口退款
$imageList0 = 'xxxxxx';//看文档吧,可以上传多张(目前只能上传一项),我这边就只上传一张了
$detail_path = "pages/order_detail/order_detail";
$params = "{\\\"id\\\":$order_id}"; //传参要用三个“\”
$payExpireSeconds = 0; //使用默认超时时间300s
$sqlPayRecord = "insert into xxxxxxxx"; //这里要把订单信息存入数据库,因为抖音支付无法自定义传参,所以后续支付结果通知所需的一些参数需要在数据库里找
$rsPayRecord = mysql_query($sqlPayRecord);
}else if($pay_type === 'xxx2'){
//如果一个小程序有多处支付可以分别传递不同参数
}
$appId = 'xxxxxxxxx'; //填写你的appid
// 生成好的 data
$data = "{\"skuList\":[{\"skuId\":\"$skuId\",\"price\":$totalAmount,\"quantity\":$quantity,\"title\":\"$title\",\"imageList\":[\"$imageList0\"],\"type\":$typeId,\"tagGroupId\":\"$tagGroupId\"}],\"outOrderNo\":\"$trade_no\",\"totalAmount\":$totalAmount,\"limitPayWayList\":$limitPayWayList,\"payExpireSeconds\":$payExpireSeconds,\"orderEntrySchema\":{\"path\":\"$detail_path\",\"params\":\"$params\"}}";
$byteAuthorization = getByteAuthorization($privateKeyStr, $data, $appId, $nonceStr, $timestamp, $keyVersion);
$array = array(
"resultCode"=>200,
"message"=>'ok',
"data"=>$data,
"byteAuthorization"=> $byteAuthorization
);
echo json_encode($array,256);
?>
正确获取data及byteAuthorization后就会拉起抖音小程序收银台:
支付结果回调:notify.php
<?php
date_default_timezone_set('prc');
// 日志记录函数
function logMessage($message) {
$logFile = __DIR__ . '/callback.log';
file_put_contents($logFile, date('Y-m-d H:i:s') . " - $message\n", FILE_APPEND);
}
// 获取抖音平台公钥
function getPublicKey() {
// 替换为抖音平台提供的公钥内容
$publicKey = "-----BEGIN PUBLIC KEY-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END PUBLIC KEY-----
";
return $publicKey;
}
// 验证签名
function verifySignature($timestamp, $nonce, $body, $signature, $publicKey) {
$message = $timestamp . "\n" . $nonce . "\n" . $body . "\n";
$pubKeyResource = openssl_get_publickey($publicKey);
if (!$pubKeyResource) {
logMessage("Failed to get public key");
return false;
}
$result = openssl_verify($message, base64_decode($signature), $pubKeyResource, OPENSSL_ALGO_SHA256);
openssl_free_key($pubKeyResource);
return $result === 1;
}
// 解析请求数据
function resolveReq() {
$body = file_get_contents('php://input'); // 获取请求体
$timestamp = $_SERVER['HTTP_BYTE_TIMESTAMP'] ?? null; // 获取时间戳
$nonce = $_SERVER['HTTP_BYTE_NONCE_STR'] ?? null; // 获取随机字符串
$signature = $_SERVER['HTTP_BYTE_SIGNATURE'] ?? null; // 获取签名
if (empty($body) || empty($timestamp) || empty($nonce) || empty($signature)) {
logMessage("Invalid request data");
return null;
}
$reqInfo = new stdClass();
$reqInfo->body = $body;
$reqInfo->timestamp = $timestamp;
$reqInfo->nonce = $nonce;
$reqInfo->signature = $signature;
return $reqInfo;
}
// 处理支付结果
function handlePaymentResult($body) {
$data1 = json_decode($body, true);
$data2 = $data1['msg'];
$data = json_decode($data2, true);
//请自行命名了.... $data1就是整个body,$data就是msg数据
if (empty($data)) {
logMessage("Invalid JSON data");
return;
}
$order_id = $data['order_id'] ?? null;
$out_order_no = $data['out_order_no'] ?? null;
$status = $data['status'] ?? null;
if (empty($order_id) || empty($out_order_no) || empty($status)) {
logMessage($out_order_no."Missing required data in notification");
return;
}
// 处理支付结果
if ($status == 'SUCCESS') {
// 支付成功,更新订单状态
updateOrderStatus($body,$order_id, 'paid');
} else {
// 支付失败,更新订单状态
updateOrderStatus($body,$order_id, 'failed');
}
}
$out_trade_no = null;
// 更新订单状态函数
function updateOrderStatus($body,$order_id, $status) {
// 替换为你的订单状态更新逻辑
// 示例:更新数据库中的订单状态
logMessage("Order $order_id status updated to $status");
$time = date('Y-m-d H:i:s');
$data1 = json_decode($body, true);
$msg_json = $data1['msg']; //'$msg_json'可以直接以这样的字符串形式保存
$msg = json_decode($msg_json, true);
//请自行命名了.... $data1就是整个body
$out_trade_no = $msg['out_order_no'];
$root = $_SERVER['DOCUMENT_ROOT'];
$payment = 'tt_pay';
if($data1['type'] === 'payment'){
if($msg['status'] === 'SUCCESS'){
$price = $msg['total_amount'] / 100;
if(mb_strpos($msg['out_order_no'], '0000001') !== false){ //可通过订单号判断支付来源
//请自行实现支付成功逻辑
}
}
}
}
// 主逻辑
$reqInfo = resolveReq();
if ($reqInfo === null) {
echo "Invalid request";
exit;
}
$publicKey = getPublicKey();
if (!verifySignature($reqInfo->timestamp, $reqInfo->nonce, $reqInfo->body, $reqInfo->signature, $publicKey)) {
echo "Signature verification failed";
exit;
}
//可以在此链接数据库
header('Content-Type: application/json');
echo json_encode(["err_no" => 0, "err_tips" => "success"]);
handlePaymentResult($reqInfo->body);
?>
发起退款:refund.php
<?php
$rootPath = $_SERVER['DOCUMENT_ROOT'];
//获取access-token
$appid = 'xxxxxxx';
$app_secret = 'xxxxxxxxxx';
$postData = array(
"client_key"=>$appid,
"client_secret"=>$app_secret,
"grant_type"=>"client_credential"
);
$postData1 = json_encode($postData,256);
$options = array(
'http' => array(
'method' => 'POST',
'header'=> array("content-type: application/json"),
'content' => $postData1
),
"ssl"=>array(
"verify_peer"=>false,
"verify_peer_name"=>false,
)
);
$context = stream_context_create($options);
$user_snsapi_base = @file_get_contents("https://open.douyin.com/oauth/client_token/",false,$context);
if(!$user_snsapi_base){
$json = json_encode(array(
"resultCode"=>900,
"message"=>'获取头条用户access_token失败',
),256);
exit($json);
}
$user_snsapi_base_json = json_decode($user_snsapi_base);
$access_token = $user_snsapi_base_json->data->access_token;
//获取退款参数里的item_order_id,该参数只能通过调用接口得到,与orderId参数不一致
// 初始化 cURL 会话
$ch = curl_init();
// 设置请求的 URL
$url = 'https://open.douyin.com/api/trade_basic/v1/developer/refund_create/';
// 设置请求头
$headers = [
'Content-Type: application/json',
'access-token: '.$access_token
];
function logMessage($message) {
$logFile = __DIR__ . '/callback_refund.log';
file_put_contents($logFile, date('Y-m-d H:i:s') . " - $message\n", FILE_APPEND);
}
// 设置请求的 JSON 数据
$data = [
"order_id" => $payment_trade_no
];
// 将数组转换为 JSON 格式
$jsonData = json_encode($data);
// 设置 cURL 选项
curl_setopt($ch, CURLOPT_URL, "https://open.douyin.com/api/trade_basic/v1/developer/order_query/");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 执行 cURL 会话
$response = curl_exec($ch);
$response = json_decode($response,true);
$item_order_id = $response['data']['item_order_list'][0]['item_order_id'];
//发起退款可以传递自定义参数
$cp_extra = [
"xxx1"=>"xxx1",
"xxx2"=>"xxx2",
];
$cp_extra = json_encode($cp_extra,256);
$data = [
"order_id" => xxx,
"out_refund_no" => xxxx,
"order_entry_schema" => [
"path" => "pages/order-detail/order-detail",
"params" => "{\"id\":$id}"
],
"refund_total_amount" => xxxx,
"notify_url" =>xxxx,
"cp_extra"=>$cp_extra,
"item_order_detail"=> [
[
"item_order_id"=> $item_order_id,
"refund_amount"=>xxxx
]
],
"refund_reason" => [
[
"code" => 999, //枚举值,请查看文档
"text" => "xxxx"
]
]
];
logMessage("data: ".json_encode($data,256));
// 将数组转换为 JSON 格式
$jsonData = json_encode($data);
// 设置 cURL 选项
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 执行 cURL 会话
$response = curl_exec($ch);
// 检查是否有错误发生
if (curl_errno($ch)) {
$error_msg = curl_error($ch);
}
// 关闭 cURL 会话
curl_close($ch);
logMessage("response: $response");
// 如果有错误,输出错误信息
if (isset($error_msg)) {
echo 'Curl error: ' . $error_msg;
}
?>
退款结果通知:notify_refund.php
<?php
date_default_timezone_set('prc');
// 日志记录函数
function logMessage($message) {
$logFile = __DIR__ . '/callback_refund.log';
file_put_contents($logFile, date('Y-m-d H:i:s') . " - $message\n", FILE_APPEND);
}
// 获取抖音平台公钥
function getPublicKey() {
// 替换为抖音平台提供的公钥内容
$publicKey = "-----BEGIN PUBLIC KEY-----
xxxxxxxxxxxxxx
-----END PUBLIC KEY-----
";
return $publicKey;
}
// 验证签名
function verifySignature($timestamp, $nonce, $body, $signature, $publicKey) {
$message = $timestamp . "\n" . $nonce . "\n" . $body . "\n";
$pubKeyResource = openssl_get_publickey($publicKey);
if (!$pubKeyResource) {
logMessage("Failed to get public key");
return false;
}
$result = openssl_verify($message, base64_decode($signature), $pubKeyResource, OPENSSL_ALGO_SHA256);
openssl_free_key($pubKeyResource);
return $result === 1;
}
// 解析请求数据
function resolveReq() {
$body = file_get_contents('php://input'); // 获取请求体
$timestamp = $_SERVER['HTTP_BYTE_TIMESTAMP'] ?? null; // 获取时间戳
$nonce = $_SERVER['HTTP_BYTE_NONCE_STR'] ?? null; // 获取随机字符串
$signature = $_SERVER['HTTP_BYTE_SIGNATURE'] ?? null; // 获取签名
if (empty($body) || empty($timestamp) || empty($nonce) || empty($signature)) {
logMessage("Invalid request data");
return null;
}
$reqInfo = new stdClass();
$reqInfo->body = $body;
$reqInfo->timestamp = $timestamp;
$reqInfo->nonce = $nonce;
$reqInfo->signature = $signature;
return $reqInfo;
}
// 处理支付结果
function handlePaymentResult($body) {
$data1 = json_decode($body, true);
$data2 = $data1['msg'];
$data = json_decode($data2, true);
if (empty($data)) {
logMessage("Invalid JSON data");
return;
}
$order_id = $data['order_id'] ?? null;
$status = $data['status'] ?? null;
if (empty($order_id) || empty($status)) {
logMessage("Missing required data in notification");
return;
}
// 处理支付结果
if ($status == 'SUCCESS') {
// 支付成功,更新订单状态
updateOrderStatus($body,$order_id, 'success');
} else {
// 支付失败,更新订单状态
updateOrderStatus($body,$order_id, 'failed');
}
}
// 更新订单状态函数
function updateOrderStatus($body,$order_id, $status) {
// 替换为你的订单状态更新逻辑
// 示例:更新数据库中的订单状态
logMessage("Order $order_id status updated to $status");
$time = date('Y-m-d H:i:s');
logMessage('body'. $body);
$data1 = json_decode($body, true);
$msg_json = $data1['msg'];
// $msg就是文档msg对象
$msg = json_decode($msg_json, true);
// $cp_extra自定义的参数
$cp_extra = json_decode($msg['cp_extra'],true);
$root = $_SERVER['DOCUMENT_ROOT'];
logMessage('type:'. $data1['type']);
logMessage('status:'. $msg['status']);
if($data1['type'] === 'refund'){
if($msg['status'] !== 'SUCCESS'){
exit;
}
//实现退款成功逻辑
}
}
// 主逻辑
$reqInfo = resolveReq();
if ($reqInfo === null) {
echo "Invalid request";
exit;
}
$publicKey = getPublicKey();
if (!verifySignature($reqInfo->timestamp, $reqInfo->nonce, $reqInfo->body, $reqInfo->signature, $publicKey)) {
echo "Signature verification failed";
exit;
}
//这里可以连接数据库
header('Content-Type: application/json');
echo json_encode(["err_no" => 0, "err_tips" => "success"]);
handlePaymentResult($reqInfo->body);
?>
抖音小程序开发注意点:
1.image标签必须设置mode属性,css的image{height:auto}无效;(该条在文档有说明)
2.点击发行抖音小程序,在上传代码前检查/static/目录下文件夹是否存在丢失,如果少了文件夹需要自行补上(如博主就丢失了svg文件夹导致真机所有svg图标都不显示了,而模拟器仍可以显示,真坑)Hbuilder X 4.51;
3.有时需要使用http请求里的set-cookie值作为服务器session_id,而微信小程序可以正常使用Set-Cookie,而抖音小程序必须使用小写set-cookie,务必注意;
4.uni组件如easyinput宽度问题,有时需要在组件属性上写style="width:100%"才可以实现100%宽度,即使在uni组件内部元素写了width:100%仍不行;
5.uni.showToast icon属性error图标值是fail而不是微信小程序等的error;
6.一些类目小程序必须通过“试运营”期才可以去掉原生导航栏,即在“试运营期”都会有原生导航栏(在真机调试时会有弹窗提示),可自行修改代码,或者调试时在弹窗选择“调整原生导航栏”小程序IDE会自动修改代码;需要试运营期的小程序类目
7.同样是“试运营期”问题,试运营期无法获取用户手机号一键登录,需自行实现其他登录方式;
8.调用uni.getUserProfile提示“getUserProfile:fail must be invoked by user tap gesture”问题,需要在button根节点上加一个异步事件data-eventsync="true"才行。参考
ps:
抖音小程序审核非常麻烦,比如:
1.小程序必须接入抖音登录,获取用户信息登录和获取用户手机号登录二选一,而第一种方式获取不了手机号,你还得开发获取用户信息登录,又要走流程,第二个又必须上线(通过试运营期)后才能使用该功能,死循环了属于是;
2.小程序前端必须要接入抖音的退款组件,无语了~
3.服务类目必须要在可选类目中明确有的,灰色的或者搭上边的就不要选了,不行~
4.不可出现任何第三方链接,即使是文字形式也不行