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
@@ -1,333 +1,61 @@
<template>
<view class="test_page">
<!-- 顶部导航 -->
<NavHeader leftTitle="抖音支付测试" leftPathType="navigateTo" />
<view class="test_container">
<!-- 状态与设备信息卡片 -->
<view class="card_box">
<view class="card_title">
<text class="title_text">📱 设备与环境信息</text>
<view class="tag_badge" :class="isApp ? 'badge_app' : 'badge_h5'">
{{ envText }}
</view>
</view>
<view class="info_grid">
<view class="info_row">
<text class="info_label">运行平台:</text>
<text class="info_val">{{ platformText }}</text>
</view>
<view class="info_row">
<text class="info_label">抖音安装状态:</text>
<text class="info_val" :style="{ color: isDyInstalled ? '#00B578' : '#FA5151' }">
{{ dyInstallStatusText }}
</text>
</view>
</view>
<view class="btn_row" style="margin-top: 20rpx;">
<button class="mini_btn check_btn" @click="checkDouyinInstalled">
🔍 检测是否安装抖音
</button>
<button class="mini_btn open_btn" @click="testDirectOpenDouyin">
🚀 直接打开抖音客户端
</button>
</view>
</view>
<!-- 支付测试参数配置 -->
<view class="card_box">
<view class="card_title">
<text class="title_text">⚙️ 支付测试参数</text>
</view>
<!-- 快捷预设模版 -->
<view class="preset_section">
<text class="section_label">快速填充测试数据:</text>
<view class="preset_btn_group">
<view class="preset_chip" @click="applyPreset('scheme')">普通App唤起 Scheme</view>
<view class="preset_chip" @click="applyPreset('cashier')">模拟收银台 Scheme</view>
<view class="preset_chip" @click="applyPreset('h5')">H5收银台链接</view>
</view>
</view>
<view class="tip_box">
<text class="tip_text">💡 提示:普通的 "snssdk1128://open" 仅能拉起抖音 App 首页。如需弹出【抖音收银台支付窗口】,orderStr 必须包含后端从字节/抖音开放平台获取的真实 pay_token 或收银台参数(如 snssdk1128://pay?pay_token=xxx 或 https://cashier.douyin.com/pay?token=xxx)。</text>
</view>
<view class="form_item">
<text class="form_label">支付类型 (type)</text>
<radio-group class="radio_group" @change="onTypeChange">
<label class="radio_item">
<radio value="app" :checked="payType === 'app'" color="#7934F6" style="transform: scale(0.8);" />
<text>app</text>
</label>
<label class="radio_item">
<radio value="h5" :checked="payType === 'h5'" color="#7934F6" style="transform: scale(0.8);" />
<text>h5</text>
</label>
</radio-group>
</view>
<view class="form_item">
<text class="form_label">支付串 / Scheme / URL (orderStr)</text>
<textarea
class="form_textarea"
v-model="orderStr"
placeholder="请输入或粘贴抖音支付链接 / Scheme(如 snssdk1128://... 或 https://...)"
></textarea>
</view>
<view class="form_row">
<view class="form_item half">
<text class="form_label">订单ID (orderId)</text>
<input class="form_input" type="number" v-model="orderId" placeholder="例如: 10001" />
</view>
<view class="form_item half">
<text class="form_label">订单编号 (orderNum)</text>
<input class="form_input" v-model="orderNum" placeholder="例如: ORD20260905001" />
</view>
</view>
</view>
<!-- 支付操作区域 -->
<view class="action_section">
<button class="main_pay_btn" :loading="loading" @click="handleTestPay">
⚡ 调用 douyinPayFun 测试支付
</button>
</view>
<!-- 运行日志与调试面板 -->
<view class="card_box">
<view class="card_title">
<text class="title_text">📋 运行日志与回调</text>
<text class="clear_link" @click="clearLogs">清空日志</text>
</view>
<scroll-view scroll-y="true" class="log_console">
<view v-if="logs.length === 0" class="empty_log">暂无测试日志,点击上方按钮开始测试</view>
<view v-for="(log, idx) in logs" :key="idx" class="log_item" :class="'log_' + log.level">
<text class="log_time">[{{ log.time }}]</text>
<text class="log_content">{{ log.msg }}</text>
</view>
</scroll-view>
</view>
</view>
</view>
<view class="test_page">
<NavHeader leftTitle="抖音支付测试" leftPathType="navigateTo" />
<view class="test_container">
<view class="card_box">
<view class="card_title">抖音 App 原生支付联调</view>
<view class="tip_box">
<text class="tip_text">粘贴 /order/payment 返回的完整 JSON(含 data.orderNum 和 data.douyin)。使用已集成 tb-douyin-pay 的自定义基座或完整安装包。测试将使用真实订单发起支付;最终结果以服务端查单为准。</text>
</view>
<view class="form_item">
<text class="form_label">接口返回 JSON</text>
<textarea class="form_textarea" v-model="paymentJson" placeholder="粘贴接口返回 JSON;也支持直接粘贴含 orderNum、douyin 的 data 对象" />
</view>
<button class="main_pay_btn" :loading="loading" :disabled="loading" @click="handleTestPay">发起该订单支付</button>
</view>
<view class="card_box">
<view class="card_title">联调结果</view>
<text>{{ resultText }}</text>
</view>
</view>
</view>
</template>
<script>
import NavHeader from "@/components/header.vue";
import { douyinPayFun, jumpWayOk, jumpWayError } from "@/utils/payUtils.js";
import { appDypayFun } from "@/utils/douyin-pay.js";
export default {
components: {
NavHeader,
},
data() {
return {
payType: "app",
orderStr: "snssdk1128://open", // 默认测试 scheme
orderId: 99999,
orderNum: "TEST_ORDER_" + Date.now(),
loading: false,
isApp: false,
platformText: "未知",
envText: "H5 / 网页",
dyInstallStatusText: "待检测",
isDyInstalled: false,
logs: [],
};
},
onLoad() {
this.initPlatformInfo();
this.addLog("页面初始化完成", "info");
},
methods: {
initPlatformInfo() {
// #ifdef APP-PLUS
this.isApp = true;
this.envText = "App 端 (App-Plus)";
const platform = uni.getSystemInfoSync().platform;
this.platformText = platform === "ios" ? "iOS App" : "Android App";
this.checkDouyinInstalled();
// #endif
// #ifdef H5
this.isApp = false;
this.envText = "H5 浏览器";
this.platformText = "Web / H5";
this.dyInstallStatusText = "Web端不支持检测安装";
// #endif
},
addLog(msg, level = "info") {
const now = new Date();
const timeStr = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
const formattedMsg = typeof msg === "object" ? JSON.stringify(msg, null, 2) : String(msg);
this.logs.unshift({
time: timeStr,
msg: formattedMsg,
level: level,
});
},
clearLogs() {
this.logs = [];
},
onTypeChange(e) {
this.payType = e.detail.value;
this.addLog(`切换支付类型为: ${this.payType}`, "info");
},
applyPreset(presetType) {
if (presetType === "scheme") {
this.payType = "app";
this.orderStr = "snssdk1128://open";
this.addLog("已加载基础唤起 Scheme: snssdk1128://open (仅打开App首页)", "info");
} else if (presetType === "cashier") {
this.payType = "app";
this.orderStr = "snssdk1128://pay?pay_token=DEMO_PAY_TOKEN_123456&order_id=TEST_ORDER_001";
this.addLog("已加载模拟收银台 Scheme 结构 (真实支付需要后端接口返回的合法 pay_token)", "info");
} else if (presetType === "h5") {
this.payType = "h5";
this.orderStr = "https://cashier.douyin.com/pay?token=DEMO_PAY_TOKEN_123456";
this.addLog("已加载 H5 收银台链接预设", "info");
}
},
// 检测抖音是否已安装 (iOS & Android)
checkDouyinInstalled() {
// #ifdef APP-PLUS
const platform = uni.getSystemInfoSync().platform;
let installed = false;
let foundPkg = "";
try {
if (platform === "android") {
const androidPkgs = [
{ name: "抖音", pname: "com.ss.android.ugc.aweme" },
{ name: "抖音极速版", pname: "com.ss.android.ugc.aweme.lite" },
{ name: "抖音火山版", pname: "com.ss.android.ugc.live" },
];
for (const pkg of androidPkgs) {
if (plus.runtime.isApplicationExist({ pname: pkg.pname })) {
installed = true;
foundPkg = pkg.name;
break;
}
}
} else if (platform === "ios") {
const iosSchemes = [
{ name: "抖音", action: "snssdk1128://" },
{ name: "抖音(douyin)", action: "douyin://" },
{ name: "抖音极速版", action: "snssdk141://" },
];
for (const sch of iosSchemes) {
if (plus.runtime.isApplicationExist({ action: sch.action })) {
installed = true;
foundPkg = sch.name;
break;
}
}
}
this.isDyInstalled = installed;
this.dyInstallStatusText = installed ? `已检测到 (${foundPkg}) ✅` : "未检测到 ❌";
this.addLog(`检测抖音客户端安装状态: ${this.dyInstallStatusText}`, installed ? "success" : "warn");
if (!installed && platform === "ios") {
this.addLog("说明: iOS检测需要将 Scheme 加入 LSApplicationQueriesSchemes。若在【标准基座】中调试,因标准基座无此配置会返回未检测到,需使用【自定义基座】运行", "info");
}
} catch (e) {
this.dyInstallStatusText = "检测异常";
this.addLog(`检测安装状态发生异常: ${e.message}`, "error");
}
// #endif
// #ifndef APP-PLUS
this.addLog("当前环境为非 App 环境,无法直接调用 5+ isApplicationExist", "warn");
// #endif
},
// 直接测试打开抖音客户端
testDirectOpenDouyin() {
this.addLog(`尝试打开抖音: ${this.orderStr || 'snssdk1128://open'}`, "info");
const targetUrl = this.orderStr || "snssdk1128://open";
// #ifdef APP-PLUS
let opened = false;
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(targetUrl);
if (app && nsUrl) {
if (app.openURL(nsUrl)) {
opened = true;
this.addLog("iOS 原生 Native.js 成功拉起客户端", "success");
}
}
} catch (e) {
console.warn(e);
}
} else 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(targetUrl));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
main.startActivity(intent);
opened = true;
this.addLog("Android 原生 Native.js Intent 成功拉起客户端", "success");
} catch (e) {
console.warn(e);
}
}
if (!opened) {
plus.runtime.openURL(
targetUrl,
(err) => {
this.addLog(`打开抖音失败: ${JSON.stringify(err)}`, "error");
const isSim = plus.navigator && plus.navigator.isSimulator ? plus.navigator.isSimulator() : false;
if (isSim) {
this.addLog("提示: iOS模拟器未安装抖音客户端,无法响应 snssdk1128:// Scheme,请使用安装了抖音的真机测试", "warn");
} else if (err && err.code === -3) {
this.addLog("提示: 错误 code -3 说明设备上未检测到抖音应用或Scheme未添加白名单,请确认真机上已安装抖音", "warn");
}
uni.showToast({
title: isSim ? "模拟器不支持打开Scheme,请在真机测试" : "拉起失败,请确认是否已安装抖音",
icon: "none",
duration: 3000,
});
}
);
}
// #endif
// #ifdef H5
window.location.href = targetUrl;
this.addLog("已在 H5 环境尝试跳转: " + targetUrl, "info");
// #endif
},
// 调用 payUtils.js 中的 douyinPayFun
async handleTestPay() {
if (!this.orderStr) {
uni.showToast({
title: "请先输入支付串或链接",
icon: "none",
});
return;
}
const payParams = {
type: this.payType,
orderStr: this.orderStr,
};
this.loading = true;
this.addLog(`开始调用 douyinPayFun,入参: ${JSON.stringify(payParams)}`, "info");
try {
const result = await douyinPayFun(payParams, this.orderId, this.orderNum);
this.addLog(`douyinPayFun 调用完成返回: ${JSON.stringify(result)}`, "success");
} catch (err) {
this.addLog(`douyinPayFun 抛出错误: ${JSON.stringify(err)}`, "error");
} finally {
this.loading = false;
}
},
},
components: { NavHeader },
data() {
return { paymentJson: "", loading: false, resultText: "尚未调用支付" };
},
methods: {
async handleTestPay() {
if (this.loading) return;
// #ifndef APP-PLUS
this.resultText = "请使用已集成原生插件的 Android/iOS App 测试";
return;
// #endif
// #ifdef APP-PLUS
this.loading = true;
try {
const response = JSON.parse(this.paymentJson);
if (response.bizcode != null && response.bizcode !== 100) throw new Error("请使用下单成功的接口返回");
const payment = response.data || response;
if (!payment.orderNum || !payment.douyin) throw new Error("缺少 orderNum 或 douyin");
this.resultText = "正在调用原生 SDK,返回后查询订单状态";
const result = await appDypayFun(payment);
const labels = { paid: "服务端确认支付成功", failed: "服务端确认支付未成功", pending: "结果待确认,请稍后查看订单", error: "调用失败" };
this.resultText = result ? (result.message || labels[result.state] || "结果待确认") : "支付正在处理中";
} catch (error) {
this.resultText = error.message || "调用失败";
} finally {
this.loading = false;
}
// #endif
},
},
};
</script>