fix: 接入抖音原生支付并修复App支付服务缺失

This commit is contained in:
Codex
2026-09-09 12:00:13 +08:00
parent b350f1c07a
commit fe8cc21bfe
155 changed files with 11995 additions and 634 deletions
+40
View File
@@ -0,0 +1,40 @@
// Pure adapter: keeps merchant credentials and signing on the server.
export function toSdkPayInfo(payment) {
const data = payment?.douyin;
if (!data) throw new Error('后端未返回抖音支付参数');
if (data.type && data.type !== 'app') throw new Error('当前仅支持抖音 App 支付');
let nested = data.orderInfo || {};
if (typeof nested === 'string') {
try { nested = JSON.parse(nested); }
catch (_) { throw new Error('抖音支付 orderInfo 格式错误'); }
}
const field = (camel, lower) => nested[lower] ?? nested[camel] ?? data[camel] ?? data[lower];
const info = {
appid: field('appId', 'appid'),
partnerid: field('mchId', 'partnerid'),
prepayid: field('prepayId', 'prepayid'),
package: field('packageValue', 'package'),
noncestr: field('nonceStr', 'noncestr'),
timestamp: field('timeStamp', 'timestamp'),
sign: field('sign', 'sign')
};
if (typeof info.timestamp === 'number' && Number.isSafeInteger(info.timestamp)) info.timestamp = String(info.timestamp);
for (const [key, value] of Object.entries(info)) {
if (typeof value !== 'string' || !value.trim()) {
throw new Error(`缺少支付参数 ${key},请先部署后端 SDK 签名补丁`);
}
}
if (info.package !== 'Sign=DYPay') throw new Error('抖音支付 package 不正确');
return info;
}
// request is the App's existing authenticated request wrapper, returning ApiResult.data.
export async function queryUntilSettled(request, orderNum, { attempts = 5, pause = 1500 } = {}) {
for (let i = 0; i < attempts; i++) {
const result = await request({ url: '/payment/queryResult', method: 'GET', data: { orderNum } });
if (result?.state === 2) return { state: 'paid' };
if (result?.state === 3) return { state: 'failed' };
if (i + 1 < attempts) await new Promise(resolve => setTimeout(resolve, pause));
}
return { state: 'pending' };
}
+127
View File
@@ -0,0 +1,127 @@
// #ifdef APP-PLUS
import { initDypay, canOpenDypay, openDypay } from '@/uni_modules/tb-douyin-pay';
// #endif
import request from './request.js';
import { BASE_URL } from './config.js';
import { TOKEN_NAME, USER_DATA } from './auth.js';
import { toSdkPayInfo, queryUntilSettled } from './douyin-pay-adapter.mjs';
let active = false;
let resuming = false;
function accountKey() {
if (!uni.getStorageSync(TOKEN_NAME)) return '';
const user = uni.getStorageSync(USER_DATA) || {};
const id = user.userId || user.id;
return id ? `tb-douyin:${BASE_URL}:${id}` : '';
}
async function query(orderNum) {
return queryUntilSettled(async options => {
const response = await request({ ...options, isShowLoading: false });
if (response?.bizcode !== 100) throw new Error('查询支付结果失败');
return response.data;
}, orderNum);
}
export function douyinPayTypes(types) {
// #ifdef H5
return types.split(',').includes('6') ? types : `${types},6`;
// #endif
// #ifdef APP-PLUS
return types.split(',').includes('6') ? types : `${types},6`;
// #endif
// #ifndef APP-PLUS
return types;
// #endif
}
export function filterDouyinPayWays(ways) {
// #ifdef H5
return ways || [];
// #endif
// #ifdef APP-PLUS
return ways || [];
// #endif
// #ifndef APP-PLUS
return (ways || []).filter(item => item.type !== 'douyin');
// #endif
}
function showState(state, key, orderNum) {
if (key !== accountKey()) return;
if (state.state !== 'pending' && uni.getStorageSync(key) === orderNum) uni.removeStorageSync(key);
uni.$emit('douyin-payment-result', { orderNum, state: state.state });
uni.showToast({ title: state.state === 'paid' ? '支付成功' : state.state === 'failed'
? '支付未成功,请查看订单' : '支付结果待确认,请稍后查看订单', icon: 'none', duration: 2500 });
}
// Runs BEFORE creating an order. Never start another payment while an earlier result is unknown.
export async function prepareDouyinPay(payway) {
if (payway !== 'douyin') return true;
try {
// #ifndef APP-PLUS
throw new Error('抖音支付仅支持 Android/iOS App');
// #endif
// #ifdef APP-PLUS
if (active || resuming) throw new Error('正在处理支付,请勿重复点击');
const key = accountKey();
if (!key) throw new Error('请先登录后再支付');
const previous = uni.getStorageSync(key);
if (previous) {
resuming = true;
try { showState(await query(previous), key, previous); }
finally { resuming = false; }
return false;
}
if (!canOpenDypay()) throw new Error('请先安装或升级抖音客户端');
return true;
// #endif
} catch (error) {
uni.showToast({ title: error.message || '暂时无法支付,请稍后重试', icon: 'none' });
return false;
}
}
// Takes the existing settlement/recharge response; does NOT create a second order.
export async function appDypayFun(payment) {
// #ifndef APP-PLUS
uni.showToast({ title: '抖音支付仅支持 Android/iOS App', icon: 'none' });
return;
// #endif
// #ifdef APP-PLUS
if (active) return;
active = true;
const key = accountKey();
try {
if (!key || !payment.orderNum) throw new Error('登录身份或支付订单编号缺失');
const payInfo = toSdkPayInfo(payment);
const previous = uni.getStorageSync(key);
if (previous) {
const state = await query(previous);
showState(state, key, previous);
if (state.state === 'pending' || previous === payment.orderNum) return state;
}
uni.setStorageSync(key, payment.orderNum);
if (!initDypay({ appId: payInfo.appid, callbackScheme: payment.douyin.callbackScheme || 'trustbridgeapp' })) {
throw new Error('支付初始化失败,请先查询订单');
}
await new Promise(resolve => openDypay({ payInfo, showLoading: true }, resolve));
const state = await query(payment.orderNum);
showState(state, key, payment.orderNum);
return state;
} catch (error) {
if (key === accountKey()) uni.showToast({ title: error.message || '支付结果待确认,请查看订单', icon: 'none' });
return { state: 'error', message: error.message || '支付结果待确认,请查看订单' };
} finally { active = false; }
// #endif
}
export async function resumeDouyinPay() {
// #ifdef APP-PLUS
if (active || resuming) return;
const key = accountKey();
const pending = key && uni.getStorageSync(key);
if (!pending) return;
resuming = true;
try { showState(await query(pending), key, pending); }
catch (_) { /* Keep the order for a later onShow. Never infer a payment failure. */ }
finally { resuming = false; }
// #endif
}
+5 -241
View File
@@ -1,3 +1,4 @@
import { appDypayFun } from './douyin-pay.js';
import { feedback } from "@/api/payment.js";
/**
@@ -110,250 +111,13 @@ 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 支付的订单编号
* Compatibility entry for callers passing ApiResult.data.douyin separately.
* Native SDK invocation and authoritative result query are handled together.
*/
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 function douyinPayFun(douyin, orderId, orderNum) {
return appDypayFun({ douyin, orderId, orderNum });
}
// 导出别名方便不同调用习惯
export const dyPayFun = douyinPayFun;
export const appDyPayFun = douyinPayFun;
export const appDouyinPayFun = douyinPayFun;
+16 -8
View File
@@ -56,10 +56,11 @@ function request(options) {
url: BASE_URL + url,
method,
data,
header: {
header: {
...defaultHeaders,
...headers,
},
...headers,
},
timeout: 15000,
success: (res) => {
if (isShowLoading) {
hideLoading();
@@ -67,8 +68,13 @@ function request(options) {
let data = res.data;
if (data && typeof data === "string") {
data = JSON.parse(data);
}
try { data = JSON.parse(data); }
catch (_) { reject(new Error('服务响应格式异常')); return; }
}
if (res.statusCode < 200 || res.statusCode >= 300 || !data) {
reject(new Error('服务请求失败,请稍后重试'));
return;
}
if (data.status === 500) {
uni.showToast({
title: "系统异常,请稍后再试",
@@ -76,7 +82,8 @@ function request(options) {
duration: 2000, // 持续时长,单位ms
mask: false, // 是否显示透明蒙层,防止触摸穿透
});
return;
reject(new Error('系统异常,请稍后再试'));
return;
}
// 601 微信登录没注册
if (data.bizcode === 100) {
@@ -114,7 +121,8 @@ function request(options) {
});
}, 2000);
}
} else {
reject(data);
} else {
console.log("错误-1", data, url);
uni.showToast({
title: data.msg || "系统错误,请稍后再试",
@@ -134,7 +142,7 @@ function request(options) {
icon: "none", // 可选 success/loading/none
duration: 2000, // 持续时长,单位ms
});
// reject(error); // 请求失败时返回错误信息
reject(error);
},
});
});