feat:抖音支付
This commit is contained in:
+233
@@ -0,0 +1,233 @@
|
||||
# 抖音支付:前端调用商城后端接口文档
|
||||
|
||||
日期:2026-09-05。适用:商城 App 前端。本文只描述前端调用我们后端的接口,不涉及抖音官方服务端接口、证书配置或插件开发。
|
||||
|
||||
## 一、公共约定
|
||||
|
||||
测试环境 Base URL:`https://api.o.tbmall.xin`。生产环境沿用项目正式用户端 API 配置,不要使用后台管理 API 地址。
|
||||
|
||||
请求头沿用现有登录请求封装:
|
||||
|
||||
```http
|
||||
Authorization: <项目当前环境的应用授权值>
|
||||
HSM-AUTH: <当前用户登录令牌>
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
POST 参数以表单提交,不是 JSON body;GET 使用 URL 查询参数。不要把后台管理 Token 填到 HSM-AUTH。
|
||||
|
||||
业务响应统一格式:
|
||||
|
||||
```json
|
||||
{ "bizcode": 100, "errcode": 0, "msg": "成功", "data": {} }
|
||||
```
|
||||
|
||||
`bizcode=100` 表示本次接口调用成功,不等于用户已付款。其他业务码按 msg 提示;HTTP 错误、超时或网络失败不能视作付款成功。以下所有 ID、金额、签名均为示例。
|
||||
|
||||
## 二、调用顺序
|
||||
|
||||
1. 调用 `/cart/getSupportPay`,判断返回列表是否包含 `type=douyin`。
|
||||
2. 根据业务场景选择一个下单/支付接口,提交 `payway=douyin`。
|
||||
3. 保存返回的 `data.orderNum`,把 `data.douyin` 中的支付参数交给 App 支付能力拉起抖音。
|
||||
4. 用户返回 App 后调用 `/payment/queryResult` 查询结果,只有 `data.state=2` 才提示支付成功。
|
||||
|
||||
**重点:支付方式筛选传 `types=6`;实际支付传 `payway=douyin`,不是 `payway=6`。**
|
||||
|
||||
## 三、查询支付方式
|
||||
|
||||
**POST `/cart/getSupportPay`**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------- | ---- | -------------------------------------------- |
|
||||
| types | string | 否 | 只查抖音传 `6`;多种传 `1,2,6`;全部传 `ALL` |
|
||||
| isDeduction | boolean | 否 | 默认 false,沿用现有抵扣券结算逻辑 |
|
||||
|
||||
请求表单:
|
||||
|
||||
```text
|
||||
types=6&isDeduction=false
|
||||
```
|
||||
|
||||
已启用抖音支付时的响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"bizcode": 100,
|
||||
"errcode": 0,
|
||||
"msg": "成功",
|
||||
"data": [{ "type": "douyin", "name": "抖音支付", "balance": 0 }]
|
||||
}
|
||||
```
|
||||
|
||||
没有启用对应配置时,列表可能为空。显示名称使用 name,提交支付方式使用 type;balance 不是抖音钱包余额。
|
||||
|
||||
## 四、发起支付(按场景选一个)
|
||||
|
||||
### 4.1 已创建订单,继续支付
|
||||
|
||||
**POST `/order/payment`**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------ | ------ | ---- | ----------------------- |
|
||||
| id | long | 是 | 待支付的商城业务订单 ID |
|
||||
| payway | string | 是 | 固定 `douyin` |
|
||||
|
||||
```text
|
||||
id=123456&payway=douyin
|
||||
```
|
||||
|
||||
后端接口还接受可选 password/code,但抖音支付不需要提交。成功响应见第五节。
|
||||
|
||||
### 4.2 商品直接购买并支付
|
||||
|
||||
**POST `/cart/settlementOrder`**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------ | ------- | -------- | ------------------------------------ |
|
||||
| addrId | long | 是 | 收货地址 ID |
|
||||
| goodsId | long | 是 | 商品 ID |
|
||||
| specsId | long | 是 | 规格 ID |
|
||||
| goodsNum | decimal | 是 | 购买数量 |
|
||||
| payway | string | 本流程是 | `douyin` |
|
||||
| payway2 | string | 否 | `vcoin` 表示抵扣券,沿用现有结算选择 |
|
||||
| couponUserId | long | 否 | 已选用户优惠券 ID |
|
||||
| remark | string | 否 | 现有分组备注 JSON 字符串 |
|
||||
|
||||
```text
|
||||
addrId=10001&goodsId=20001&specsId=30001&goodsNum=1&payway=douyin
|
||||
```
|
||||
|
||||
remark 格式为 `[{"gkey":"结算返回的分组标识","rmk":"备注内容"}]`,通过现有请求封装表单编码。金额由后端计算,不传前端自算金额。优惠券、抵扣券沿用结算规则,不因选择抖音改变适用范围。
|
||||
|
||||
### 4.3 购物车结算并支付
|
||||
|
||||
**POST `/cart/settle`**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------ | ------ | ---- | ---------------------------------- |
|
||||
| addrId | long | 是 | 收货地址 ID |
|
||||
| cartIds | string | 是 | 购物车 ID 逗号分隔,`ALL` 表示全选 |
|
||||
| payway | string | 是 | `douyin` |
|
||||
| couponUserId | long | 否 | 用户优惠券 ID |
|
||||
| remark | string | 否 | 分组备注 JSON 字符串,同上一接口 |
|
||||
|
||||
```text
|
||||
addrId=10001&cartIds=40001,40002&payway=douyin
|
||||
```
|
||||
|
||||
购物车可能拆分业务订单并合并支付,查询支付结果仍使用本次返回的 orderNum。
|
||||
|
||||
### 4.4 余额充值
|
||||
|
||||
**POST `/finance/recharge`**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------- | ------- | ---- | -------------------- |
|
||||
| category | string | 是 | 余额充值传 `balance` |
|
||||
| amount | decimal | 是 | 充值金额,单位元 |
|
||||
| payway | string | 是 | `douyin` |
|
||||
|
||||
```text
|
||||
category=balance&amount=1.00&payway=douyin
|
||||
```
|
||||
|
||||
注意:上述四个 POST 都可能创建支付单,请勿反复点击或自动重试创建。新下单接口已经返回支付参数时,不要紧接着再调用 `/order/payment`。
|
||||
|
||||
## 五、下单/支付接口统一返回
|
||||
|
||||
上述四个接口均返回 PayInfoDto。抖音待支付响应示例(省略其他业务字段):
|
||||
|
||||
```json
|
||||
{
|
||||
"bizcode": 100,
|
||||
"errcode": 0,
|
||||
"msg": "成功",
|
||||
"data": {
|
||||
"orderId": 123456,
|
||||
"orderNum": "PAY_EXAMPLE_001",
|
||||
"amount": 1.0,
|
||||
"status": 0,
|
||||
"douyin": {
|
||||
"appId": "EXAMPLE_APP_ID",
|
||||
"mchId": "EXAMPLE_MERCHANT_ID",
|
||||
"prepayId": "EXAMPLE_PREPAY_ID",
|
||||
"callbackScheme": "trustbridgeapp",
|
||||
"packageValue": "Sign=DYPay",
|
||||
"nonceStr": "0123456789abcdef0123456789abcdef",
|
||||
"timeStamp": "1788585600",
|
||||
"sign": "EXAMPLE_BASE64_SIGNATURE"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 前端用途 |
|
||||
| --------------------- | ------- | ---------------------------------------------- |
|
||||
| orderId | long | 后端返回的订单 ID |
|
||||
| orderNum | string | 保存下来,支付结果查询必须用它 |
|
||||
| amount | decimal | 实际支付金额(元) |
|
||||
| status | integer | 此 DTO 中 0=待支付、1=已支付;不同于查单 state |
|
||||
| douyin | object | App 拉起抖音所需参数 |
|
||||
| douyin.appId | string | 抖音应用 ID |
|
||||
| douyin.mchId | string | 抖音商户 ID |
|
||||
| douyin.prepayId | string | 预支付 ID |
|
||||
| douyin.callbackScheme | string | App 返回 Scheme |
|
||||
| douyin.packageValue | string | `Sign=DYPay` |
|
||||
| douyin.nonceStr | string | 随机字符串 |
|
||||
| douyin.timeStamp | string | 秒级时间戳,保留字符串类型 |
|
||||
| douyin.sign | string | 后端生成的签名,原样使用 |
|
||||
|
||||
后端已处理签名,前端无需请求商户私钥或另调签名接口。参数缺失时停止拉起,提示支付参数异常并反馈后端,不要自行伪造字段。
|
||||
|
||||
若 App 支付桥接要求 SDK 小写字段,映射如下;这不是另一个 HTTP 请求:
|
||||
|
||||
```js
|
||||
const d = response.data.douyin;
|
||||
const payInfo = {
|
||||
appid: d.appId,
|
||||
partnerid: d.mchId,
|
||||
prepayid: d.prepayId,
|
||||
package: d.packageValue,
|
||||
noncestr: d.nonceStr,
|
||||
timestamp: d.timeStamp,
|
||||
sign: d.sign,
|
||||
};
|
||||
```
|
||||
|
||||
## 六、查询支付结果
|
||||
|
||||
**GET `/payment/queryResult`**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| -------- | ------ | ---- | ------------------------------------------------------- |
|
||||
| orderNum | string | 是 | 发起支付接口返回的 data.orderNum,不是 orderId/prepayId |
|
||||
|
||||
```http
|
||||
GET https://api.o.tbmall.xin/payment/queryResult?orderNum=PAY_EXAMPLE_001
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"bizcode": 100,
|
||||
"errcode": 0,
|
||||
"msg": "成功",
|
||||
"data": { "state": 2 }
|
||||
}
|
||||
```
|
||||
|
||||
| state | 含义 | 前端处理 |
|
||||
| ----- | ---------- | -------------------------------- |
|
||||
| 0 | 订单不存在 | 核对支付编号及环境,不能提示成功 |
|
||||
| 1 | 支付处理中 | 稍后再次查询 |
|
||||
| 2 | 支付成功 | 展示成功,刷新订单或充值余额 |
|
||||
| 3 | 支付失败 | 提示失败,刷新订单状态 |
|
||||
|
||||
当前 App 采用最多 5 次、间隔 1.5 秒的查询;这属于前端策略,不是后端强制限制。暂未确认、断网或超时应显示“支付结果确认中”,保存编号以便回前台再次查询,避免直接重复付款。
|
||||
|
||||
用户取消或支付 SDK 提示成功后,都需要以服务端查询结果为准。不要调用 `/payment/feedback` 上报成功来替代查单;前端也不需要调用后端的抖音通知/退款回调接口。
|
||||
|
||||
## 七、对接范围与发布注意
|
||||
|
||||
- 本文按当前工作区后端代码整理;SDK 签名新增字段(packageValue、nonceStr、timeStamp、sign)尚未确认已部署到测试/生产服务器。
|
||||
- 后台配置已完成不代表预下单、签名、支付回调均验收通过,实际支付链路仍需联调。
|
||||
- 本次抖音支付是 App 原生支付;H5 仅可检查适用的后端业务请求,不能凭网页调用拉起此原生能力。
|
||||
+3
-3
@@ -454,7 +454,7 @@ export const FINANCE_WITHDRAWAL_STATUS = [
|
||||
];
|
||||
|
||||
/**
|
||||
* 页面:1-APP首页,2-APP分类页,3-商学院资讯,4-爆单区,5-好物区,6-积分专区,7-数字积分页 8-新人专享、9-超级补贴、10-限时秒杀、11-领券中心
|
||||
* 页面:1-APP首页,2-APP分类页,3-资讯,4-爆单区,5-好物区,6-积分专区,7-数字积分页 8-新人专享、9-超级补贴、10-限时秒杀、11-领券中心
|
||||
* @type {[{value: string, key: string}]}
|
||||
*/
|
||||
export const APP_PAGE_TYPE = {
|
||||
@@ -486,8 +486,8 @@ export const NEWS_TYPE = [
|
||||
key: 3,
|
||||
value: "新闻"
|
||||
}, {
|
||||
key: 5,
|
||||
value: "商学院"
|
||||
// key: 5,
|
||||
// value: "商学院"
|
||||
},
|
||||
// {
|
||||
// key: 4,
|
||||
|
||||
@@ -109,6 +109,255 @@ export function zfbPayFun(alipay, orderId, orderNum) {
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* 抖音支付(兼容 iOS、Android App 端)
|
||||
* @param {Object|String} dyPay 抖音支付参数 (支持 orderStr, schema, url, payUrl, orderInfo 等)
|
||||
* @param {Object|String|Number} orderId 支付的订单ID
|
||||
* @param {Object|String} orderNum 支付的订单编号
|
||||
*/
|
||||
export function douyinPayFun(dyPay, orderId, orderNum) {
|
||||
console.log("抖音支付---入参:", dyPay, orderId, orderNum);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!dyPay || (typeof dyPay === "object" && Object.keys(dyPay).length === 0)) {
|
||||
uni.showToast({
|
||||
title: "支付参数错误",
|
||||
icon: "none",
|
||||
});
|
||||
jumpWayError();
|
||||
return reject(new Error("支付参数为空"));
|
||||
}
|
||||
|
||||
const d = typeof dyPay === "object" ? dyPay : {};
|
||||
const sub = (typeof d.orderInfo === "object" && d.orderInfo) ? d.orderInfo : {};
|
||||
const type = typeof dyPay === "object" ? (dyPay.type || "app") : "app";
|
||||
|
||||
// 1. 提取或生成 Scheme / URL (注意:排除 sign,避免将签名误认为 URL)
|
||||
let orderStr = "";
|
||||
if (typeof dyPay === "string") {
|
||||
orderStr = dyPay;
|
||||
} else if (typeof dyPay === "object") {
|
||||
orderStr =
|
||||
dyPay.orderStr ||
|
||||
dyPay.schema ||
|
||||
dyPay.scheme ||
|
||||
dyPay.url ||
|
||||
dyPay.payUrl ||
|
||||
dyPay.link ||
|
||||
sub.orderStr ||
|
||||
sub.schema ||
|
||||
sub.scheme ||
|
||||
sub.url ||
|
||||
"";
|
||||
|
||||
// 如果未配置直接链接,则尝试通过 prepayId / pay_token 自动生成抖音收银台 Scheme 备用
|
||||
const token = d.prepayId || d.prepayid || sub.prepayid || sub.prepayId;
|
||||
if (!orderStr && token) {
|
||||
orderStr = `snssdk1128://pay?pay_token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("抖音支付---提取到的 Scheme / orderStr:", orderStr);
|
||||
|
||||
// 2. 深度合并/映射原生支付所需的 orderInfo(融合根节点与 orderInfo 子节点,同时提供驼峰与小写命名)
|
||||
const appId = d.appId || d.appid || sub.appId || sub.appid || "";
|
||||
const mchId = d.mchId || d.partnerid || d.mchid || sub.mchId || sub.partnerid || sub.mchid || "";
|
||||
const prepayId = d.prepayId || d.prepayid || sub.prepayId || sub.prepayid || "";
|
||||
const callbackScheme = d.callbackScheme || sub.callbackScheme || "trustbridgeapp";
|
||||
const packageValue = d.packageValue || d.package || sub.packageValue || sub.package || "Sign=DYPay";
|
||||
const nonceStr = d.nonceStr || d.noncestr || sub.nonceStr || sub.noncestr || "";
|
||||
const timeStamp = String(d.timeStamp || d.timestamp || sub.timeStamp || sub.timestamp || "");
|
||||
const sign = d.sign || sub.sign || "";
|
||||
|
||||
const orderInfo = {
|
||||
// 驼峰命名(标准 uni.requestPayment / 抖音 SDK)
|
||||
appId,
|
||||
mchId,
|
||||
prepayId,
|
||||
callbackScheme,
|
||||
packageValue,
|
||||
nonceStr,
|
||||
timeStamp,
|
||||
sign,
|
||||
// 小写命名(部分原生桥接 SDK 兼容)
|
||||
appid: appId,
|
||||
partnerid: mchId,
|
||||
mchid: mchId,
|
||||
prepayid: prepayId,
|
||||
package: packageValue,
|
||||
noncestr: nonceStr,
|
||||
timestamp: timeStamp,
|
||||
service: 5,
|
||||
};
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// 情形 A:如果显式指定为 H5 方式
|
||||
if (
|
||||
type === "h5" &&
|
||||
orderStr &&
|
||||
(orderStr.startsWith("http://") || orderStr.startsWith("https://"))
|
||||
) {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
`/pages/other_package/payment_processing/payment_processing?link=${encodeURIComponent(
|
||||
orderStr
|
||||
)}&orderNum=` + orderNum,
|
||||
});
|
||||
resolve({ status: "processing" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 情形 B:如果后端直接传入了协议 Scheme 串(如以 snssdk1128://, douyin:// 开头)
|
||||
if (
|
||||
typeof dyPay === "string" ||
|
||||
(dyPay.orderStr && (dyPay.orderStr.startsWith("snssdk") || dyPay.orderStr.startsWith("douyin")))
|
||||
) {
|
||||
let openSuccess = false;
|
||||
|
||||
// iOS Native.js 尝试
|
||||
if (plus.os.name === "iOS") {
|
||||
try {
|
||||
const UIApplication = plus.ios.importClass("UIApplication");
|
||||
const NSURL = plus.ios.importClass("NSURL");
|
||||
const app = UIApplication.sharedApplication();
|
||||
const nsUrl = NSURL.URLWithString(orderStr);
|
||||
if (app && nsUrl && app.openURL(nsUrl)) {
|
||||
openSuccess = true;
|
||||
}
|
||||
} catch (nativeErr) {
|
||||
console.warn("iOS Native.js openURL 尝试:", nativeErr);
|
||||
}
|
||||
}
|
||||
|
||||
// Android Native.js 尝试
|
||||
if (plus.os.name === "Android") {
|
||||
try {
|
||||
const Intent = plus.android.importClass("android.content.Intent");
|
||||
const Uri = plus.android.importClass("android.net.Uri");
|
||||
const main = plus.android.runtimeMainActivity();
|
||||
const intent = new Intent(Intent.ACTION_VIEW, Uri.parse(orderStr));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
main.startActivity(intent);
|
||||
openSuccess = true;
|
||||
} catch (androidErr) {
|
||||
console.warn("Android Native.js Intent 尝试:", androidErr);
|
||||
}
|
||||
}
|
||||
|
||||
if (!openSuccess) {
|
||||
plus.runtime.openURL(
|
||||
orderStr,
|
||||
() => {
|
||||
if (orderNum) {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
`/pages/other_package/payment_processing/payment_processing?orderNum=` +
|
||||
orderNum,
|
||||
});
|
||||
}
|
||||
resolve({ status: "invoked" });
|
||||
},
|
||||
(err) => {
|
||||
console.error("拉起抖音客户端失败:", err);
|
||||
const isSimulator =
|
||||
plus.navigator && plus.navigator.isSimulator
|
||||
? plus.navigator.isSimulator()
|
||||
: false;
|
||||
let tip = "未检测到抖音客户端或拉起失败,请确认是否已安装抖音客户端";
|
||||
if (isSimulator) {
|
||||
tip = "iOS模拟器无法打开第三方应用Scheme,请在安装了抖音的真机上测试";
|
||||
} else if (err && err.code === -3) {
|
||||
tip = "未检测到抖音应用或当前基座未配置Scheme白名单,请确认真机已安装抖音";
|
||||
}
|
||||
uni.showToast({
|
||||
title: tip,
|
||||
icon: "none",
|
||||
duration: 3000,
|
||||
});
|
||||
jumpWayError();
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if (orderNum) {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
`/pages/other_package/payment_processing/payment_processing?orderNum=` +
|
||||
orderNum,
|
||||
});
|
||||
}
|
||||
resolve({ status: "invoked" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 情形 C:使用 uni.requestPayment 调起原生支付,失败时通过生成 Scheme 降级拉起
|
||||
uni.requestPayment({
|
||||
provider: "toutiao",
|
||||
orderInfo: orderInfo,
|
||||
service: 5,
|
||||
success: function (res) {
|
||||
console.log("抖音支付成功:", res);
|
||||
payFeedbackFun(orderId, orderNum, 1);
|
||||
jumpWayOk();
|
||||
resolve(res);
|
||||
},
|
||||
fail: function (err) {
|
||||
console.error("uni.requestPayment 抖音支付失败/不兼容,尝试 Scheme 降级拉起:", err);
|
||||
if (orderStr) {
|
||||
plus.runtime.openURL(
|
||||
orderStr,
|
||||
() => {
|
||||
if (orderNum) {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
`/pages/other_package/payment_processing/payment_processing?orderNum=` +
|
||||
orderNum,
|
||||
});
|
||||
}
|
||||
resolve({ status: "invoked_fallback" });
|
||||
},
|
||||
(openErr) => {
|
||||
console.error("openURL 降级失败:", openErr);
|
||||
jumpWayError();
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
jumpWayError();
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
// #endif
|
||||
|
||||
// 兜底处理 (H5 及非 App 环境)
|
||||
if (orderStr) {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
`/pages/other_package/payment_processing/payment_processing?link=${encodeURIComponent(
|
||||
orderStr
|
||||
)}&orderNum=` + orderNum,
|
||||
});
|
||||
resolve({ status: "processing" });
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "抖音支付暂仅支持在 App 端调用",
|
||||
icon: "none",
|
||||
});
|
||||
jumpWayError();
|
||||
reject(new Error("抖音支付暂仅支持在 App 端调用"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 导出别名方便不同调用习惯
|
||||
export const dyPayFun = douyinPayFun;
|
||||
export const appDyPayFun = douyinPayFun;
|
||||
export const appDouyinPayFun = douyinPayFun;
|
||||
|
||||
// 跳转到支付成功页面
|
||||
export function jumpWayOk() {
|
||||
uni.navigateTo({
|
||||
|
||||
Reference in New Issue
Block a user