fix: 移除抖音支付额外弹窗并统一成功页跳转
This commit is contained in:
@@ -98,3 +98,17 @@ npm run test:douyin 覆盖参数适配、旧调用入口、服务端确认、重
|
||||
最终验证:27 项前端支付测试全部通过;后端 41 项选定支付测试通过,包含真实 H2 事务回滚验证。
|
||||
HBuilderX 5.24 App 资源编译及导出成功(有既有 CSS 注释警告)。
|
||||
资源导出通过不代表 Android/iOS 原生构建或真机交易已验证,发布前仍须完成验收。
|
||||
|
||||
## 与现有支付交互对齐(2026-09-09 第三次补丁)
|
||||
|
||||
本节取代前文“显式确认/继续支付/重新支付弹窗”的交互说明,遵循用户要求,不新增确认弹窗。
|
||||
|
||||
- 用户点击支付即继续:原单有效时直接续付;渠道确认旧单关闭后,直接继续当前页面的下单请求。保留服务端防重复支付校验。
|
||||
- 微信、支付宝和抖音共用现有支付成功页 `pages/order_package/order_submit_ok/order_submit_ok`,不新建成功页,不仅显示成功 Toast。
|
||||
- 抖音正常回调、再次点击查到已支付、冷启动查到已支付,均在服务端确认后跳转成功页。
|
||||
- 从抖音返回 App 时,即使 SDK 回调丢失,也允许 onShow 查单结束等待;与迟到 SDK 回调竞争时只处理一次。查单失败或 pending 不跳成功、不解锁。
|
||||
- 页面栈满时使用 redirectTo 进入同一成功页;两种跳转均失败则保留待查记录,下次重试跳转,不再次付款。
|
||||
- 本次仅修改前端 JavaScript、测试及文档,不改后端、接口、SQL、原生插件和支付页面样式。仍依赖上一补丁的后端与原生插件 0.2.0。
|
||||
|
||||
本地 33 项支付测试通过,涵盖无弹窗、成功跳转、丢失回调、回调竞争、账号切换、页面栈满以及微信/支付宝成功页回归。尚未进行真机实付。
|
||||
HBuilderX 5.24 App 资源编译及导出成功,仅有既有 CSS Autoprefixer 注释警告。
|
||||
|
||||
@@ -30,10 +30,13 @@ function loadModule(file, context, app = true) {
|
||||
function uniMock() {
|
||||
const storage = new Map([['token', 'test-token'], ['user', { userId: 7 }]]);
|
||||
const messages = [];
|
||||
return { storage, messages, getStorageSync: k => storage.get(k), setStorageSync: (k,v) => storage.set(k,v),
|
||||
const navigations = [];
|
||||
return { storage, messages, navigations, getStorageSync: k => storage.get(k), setStorageSync: (k,v) => storage.set(k,v),
|
||||
removeStorageSync: k => storage.delete(k), showToast: v => messages.push(v.title), $emit() {},
|
||||
showModal: options => options.success({ confirm: true }),
|
||||
showLoading() {}, hideLoading() {}, navigateTo() {} };
|
||||
showModal: () => assert.fail('Douyin must not add payment confirmation modals'),
|
||||
showLoading() {}, hideLoading() {},
|
||||
navigateTo: options => { navigations.push(options.url); options.success?.(); },
|
||||
redirectTo: options => { navigations.push(options.url); options.success?.(); } };
|
||||
}
|
||||
const payment = { orderNum: 'order-1', douyin: { appId: 'app', mchId: 'mch', prepayId: 'prepay',
|
||||
packageValue: 'Sign=DYPay', nonceStr: 'nonce', timeStamp: '1780000000', sign: 'sig' } };
|
||||
@@ -45,14 +48,16 @@ async function payContext(overrides = {}, app = true) {
|
||||
request: async options => options.url === '/payment/douyin/resume'
|
||||
? ({ bizcode: 100, data: { state: 'paid', orderNum: options.data.orderNum } })
|
||||
: ({ bizcode: 100, data: { state: 2 } }), ...overrides };
|
||||
loadModule('utils/payment-navigation.js', context, app);
|
||||
return loadModule('utils/douyin-pay.js', context, app);
|
||||
}
|
||||
const successPage = '/pages/order_package/order_submit_ok/order_submit_ok';
|
||||
test('SDK success is confirmed with server before showing success', async () => {
|
||||
let queries = 0;
|
||||
const ctx = await payContext({ request: async () => { queries++; return { bizcode:100, data:{state:2} }; } });
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(queries, 1);
|
||||
assert.equal(ctx.uni.messages.at(-1), '支付成功');
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
});
|
||||
test('SDK success plus network failure remains recoverable, never success', async () => {
|
||||
@@ -60,11 +65,12 @@ test('SDK success plus network failure remains recoverable, never success', asyn
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), 'order-1');
|
||||
assert.equal(ctx.uni.messages.includes('支付成功'), false);
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
});
|
||||
test('cancel still queries authoritative server state', async () => {
|
||||
const ctx = await payContext({ openDypay: (_, cb) => cb({ resultCode:'1' }) });
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.messages.at(-1), '支付成功');
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
});
|
||||
test('old backend parameters never invoke native SDK', async () => {
|
||||
let calls = 0;
|
||||
@@ -123,6 +129,7 @@ test('account switch suppresses old account result and retains its pending recor
|
||||
ctx.request = async () => { ctx.uni.storage.set('user', {userId:8}); return {bizcode:100,data:{state:2}}; };
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.messages.includes('支付成功'), false);
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), 'order-1');
|
||||
});
|
||||
test('cold start queries saved order; unavailable app does not create order', async () => {
|
||||
@@ -130,7 +137,7 @@ test('cold start queries saved order; unavailable app does not create order', as
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', 'pending');
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.equal(ctx.uni.messages.at(-1), '支付成功');
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
});
|
||||
test('non-App excludes Douyin and never accesses native SDK', async () => {
|
||||
const ctx = await payContext({}, false);
|
||||
@@ -181,16 +188,14 @@ test('killed App resumes the original unpaid transaction, without creating anoth
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
});
|
||||
|
||||
test('cold start and declining continuation never open the SDK or erase pending state', async () => {
|
||||
test('cold start never opens the SDK or asks for payment and preserves pending state', async () => {
|
||||
const ctx = await payContext({openDypay:()=>assert.fail('Unexpected SDK call'),
|
||||
queryUntilSettled: async()=>({state:'pending'})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
ctx.uni.showModal = () => assert.fail('onShow must not ask for payment');
|
||||
await ctx.resumeDouyinPay();
|
||||
ctx.request = async()=>({bizcode:100,data:{state:'unpaid',orderNum:payment.orderNum,payment}});
|
||||
ctx.uni.showModal = options => options.success({confirm:false});
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
});
|
||||
|
||||
test('unknown, processing, network error and missing backend remain locked', async () => {
|
||||
@@ -234,16 +239,16 @@ test('closed old transaction unlocks a fresh server order, never reopens old par
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
|
||||
test('declining new payment or switching accounts after closure never creates a payment', async () => {
|
||||
for(const mode of ['cancel','account']) {
|
||||
const ctx=await payContext({request:async()=>({bizcode:100,data:{state:'closed',orderNum:'expired-order'}})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','expired-order');
|
||||
ctx.uni.showModal=options=>{
|
||||
if(mode==='account') ctx.uni.storage.set('user',{userId:8});
|
||||
options.success({confirm:mode!=='cancel'});
|
||||
};
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),false);
|
||||
}
|
||||
test('switching accounts during old-order closure cannot create a new payment', async () => {
|
||||
const ctx=await payContext();
|
||||
ctx.request=async()=>{
|
||||
ctx.uni.storage.set('user',{userId:8});
|
||||
return {bizcode:100,data:{state:'closed',orderNum:'expired-order'}};
|
||||
};
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','expired-order');
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),false);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),'expired-order');
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
});
|
||||
|
||||
test('unconfirmed server state never resets the native lock and explains why', async () => {
|
||||
@@ -273,36 +278,112 @@ test('SDK rejection stays recoverable but no longer hides the native failure', a
|
||||
assert.equal(ctx.uni.messages.includes('支付成功'),false);
|
||||
});
|
||||
|
||||
test('account switch during resume response or confirmation cannot invoke SDK', async () => {
|
||||
for (const phase of ['response','confirmation']) {
|
||||
test('account switch during resume response or native reset cannot invoke SDK', async () => {
|
||||
for (const phase of ['response','reset']) {
|
||||
const ctx = await payContext({openDypay:()=>assert.fail('Old account reached SDK')});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
ctx.request = async()=> {
|
||||
if (phase === 'response') ctx.uni.storage.set('user',{userId:8});
|
||||
return {bizcode:100,data:{state:'unpaid',orderNum:payment.orderNum,payment}};
|
||||
};
|
||||
ctx.uni.showModal = options => {ctx.uni.storage.set('user',{userId:8}); options.success({confirm:true});};
|
||||
if (phase === 'reset') ctx.resetDypay = () => ctx.uni.storage.set('user',{userId:8});
|
||||
await ctx.prepareDouyinPay('douyin');
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
}
|
||||
});
|
||||
|
||||
test('concurrent clicks and onShow during continuation open only one native payment', async () => {
|
||||
let confirm;
|
||||
let done;
|
||||
let calls = 0;
|
||||
const ctx = await payContext({request: async()=>({bizcode:100,data:{state:'unpaid',orderNum:payment.orderNum,payment}}),
|
||||
queryUntilSettled: async()=>({state:'paid'}),
|
||||
openDypay:(_,cb)=>{ calls++; cb({resultCode:'0'}); }});
|
||||
openDypay:(_,cb)=>{ calls++; done = cb; }});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
ctx.uni.showModal = options => {confirm = options.success;};
|
||||
const first = ctx.prepareDouyinPay('douyin');
|
||||
await new Promise(resolve=>setImmediate(resolve));
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
await ctx.appDypayFun(payment);
|
||||
await ctx.resumeDouyinPay();
|
||||
confirm({confirm:true});
|
||||
done({resultCode:'0'});
|
||||
await first;
|
||||
assert.equal(calls, 1);
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
});
|
||||
|
||||
test('returning from Douyin with a missing callback completes via server query and navigates once', async () => {
|
||||
let lateCallback;
|
||||
const ctx = await payContext({openDypay:(_,cb)=>{ lateCallback = cb; }});
|
||||
const paying = ctx.appDypayFun(payment);
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.equal((await paying).state, 'paid');
|
||||
lateCallback({resultCode:'0'});
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
});
|
||||
|
||||
test('pending or failed foreground query never unlocks or navigates before the native callback', async () => {
|
||||
for (const mode of ['pending', 'offline']) {
|
||||
let done, resets = 0;
|
||||
const ctx = await payContext({openDypay:(_,cb)=>{ done=cb; }, resetDypay:()=>resets++,
|
||||
queryUntilSettled:async()=>{ if (mode === 'offline') throw new Error('offline'); return {state:'pending'}; }});
|
||||
const paying = ctx.appDypayFun(payment);
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.equal(resets, 0);
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
ctx.queryUntilSettled = async()=>({state:'paid'});
|
||||
done({resultCode:'0'});
|
||||
await paying;
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
}
|
||||
});
|
||||
|
||||
test('native callback racing with foreground query still navigates only once', async () => {
|
||||
let done, finishQuery;
|
||||
const ctx=await payContext({openDypay:(_,cb)=>{done=cb;}});
|
||||
const paying=ctx.appDypayFun(payment);
|
||||
ctx.queryUntilSettled=()=>new Promise(resolve=>{finishQuery=resolve;});
|
||||
const foreground=ctx.resumeDouyinPay();
|
||||
ctx.queryUntilSettled=async()=>({state:'paid'});
|
||||
done({resultCode:'0'});
|
||||
await paying;
|
||||
finishQuery({state:'paid'});
|
||||
await foreground;
|
||||
assert.deepEqual(ctx.uni.navigations,[successPage]);
|
||||
});
|
||||
|
||||
test('full page stack falls back to redirect; navigation failure remains retryable without another payment', async () => {
|
||||
const ctx=await payContext();
|
||||
ctx.uni.navigateTo=options=>options.fail();
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.deepEqual(ctx.uni.navigations,[successPage]);
|
||||
const failed=await payContext();
|
||||
failed.uni.navigateTo=options=>options.fail();
|
||||
failed.uni.redirectTo=options=>options.fail();
|
||||
await failed.appDypayFun(payment);
|
||||
assert.equal(failed.uni.storage.get('tb-douyin:test-api:7'),payment.orderNum);
|
||||
failed.uni.redirectTo=options=>{ failed.uni.navigations.push(options.url); options.success(); };
|
||||
failed.openDypay=()=>assert.fail('Already paid order must not open SDK again');
|
||||
await failed.prepareDouyinPay('douyin');
|
||||
assert.deepEqual(failed.uni.navigations,[successPage]);
|
||||
assert.equal(failed.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
|
||||
test('already visible success page is not pushed again', async () => {
|
||||
const ctx=await payContext({getCurrentPages:()=>[{route:successPage.slice(1)}]});
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.deepEqual(ctx.uni.navigations,[]);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
|
||||
test('WeChat and Alipay retain the same shared success page', async () => {
|
||||
const ctx=await payContext({feedback:async()=>({bizcode:100}),console:{log(){},error(){}}});
|
||||
loadModule('utils/payUtils.js',ctx);
|
||||
ctx.uni.requestPayment=options=>options.success({});
|
||||
ctx.appWxpayFun({type:'app'},1,'wechat-order');
|
||||
ctx.zfbPayFun({type:'app',orderStr:'test'},2,'alipay-order');
|
||||
assert.deepEqual(ctx.uni.navigations,[successPage,successPage]);
|
||||
});
|
||||
|
||||
test('resume refuses mismatched order, bad signed parameters and native initialization failure', async () => {
|
||||
|
||||
+58
-33
@@ -5,9 +5,12 @@ 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';
|
||||
import { jumpToPaymentSuccess } from './payment-navigation.js';
|
||||
|
||||
let active = false;
|
||||
let resuming = false;
|
||||
let inFlight = null;
|
||||
let checkingForeground = false;
|
||||
function accountKey() {
|
||||
if (!uni.getStorageSync(TOKEN_NAME)) return '';
|
||||
const user = uni.getStorageSync(USER_DATA) || {};
|
||||
@@ -44,12 +47,21 @@ export function filterDouyinPayWays(ways) {
|
||||
return (ways || []).filter(item => item.type !== 'douyin');
|
||||
// #endif
|
||||
}
|
||||
function showState(state, key, orderNum) {
|
||||
async function showState(state, key, orderNum) {
|
||||
if (key !== accountKey()) return;
|
||||
if (state.state === 'paid') {
|
||||
const navigated = await jumpToPaymentSuccess();
|
||||
if (key !== accountKey()) return;
|
||||
if (!navigated) {
|
||||
// Retain the record so the next onShow/click can retry navigation without paying again.
|
||||
uni.showToast({ title: '支付成功,请在订单中查看', icon: 'none' });
|
||||
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'
|
||||
? '支付未成功,请查看订单' : state.state === 'closed' ? '原支付已关闭,可重新下单'
|
||||
if (state.state === 'paid' || state.state === 'closed') return;
|
||||
uni.showToast({ title: state.state === 'failed' ? '支付未成功,请查看订单'
|
||||
: state.message || '支付结果待确认,请稍后查看订单', icon: 'none', duration: 2500 });
|
||||
}
|
||||
|
||||
@@ -65,7 +77,7 @@ async function continuePendingPayment(key, orderNum) {
|
||||
// The existing query endpoint also reconciles business orders when a webhook was missed.
|
||||
const state = result.state === 'paid' ? await query(orderNum) : { state: 'closed' };
|
||||
if (key === accountKey() && state.state !== 'pending') resetDypay();
|
||||
showState(state, key, orderNum);
|
||||
await showState(state, key, orderNum);
|
||||
return state;
|
||||
}
|
||||
if (result.state !== 'unpaid') {
|
||||
@@ -76,21 +88,14 @@ async function continuePendingPayment(key, orderNum) {
|
||||
CHANNEL_PENDING: '抖音支付处理中,请稍后查看订单'
|
||||
};
|
||||
const state = { state: 'pending', message: messages[result.reason] };
|
||||
showState(state, key, orderNum);
|
||||
await showState(state, key, orderNum);
|
||||
return state;
|
||||
}
|
||||
if (result.payment?.orderNum !== orderNum) throw new Error('续付订单不匹配,请查看订单');
|
||||
// Validate before the confirmation dialog. Never store the SDK signature locally.
|
||||
// The user's payment click already authorizes continuation. No additional modal.
|
||||
// Validate the original server parameters; never store the SDK signature locally.
|
||||
toSdkPayInfo(result.payment);
|
||||
resetDypay();
|
||||
const confirmed = await new Promise(resolve => uni.showModal({
|
||||
title: '继续上一笔支付',
|
||||
content: `上一笔抖音支付尚未完成,是否继续支付原订单?${result.payment.amount != null
|
||||
? `金额:¥${result.payment.amount}。` : ''}不会创建新订单。`,
|
||||
confirmText: '继续支付', cancelText: '暂不支付',
|
||||
success: res => resolve(res.confirm === true), fail: () => resolve(false)
|
||||
}));
|
||||
if (!confirmed || key !== accountKey()) return { state: 'pending' };
|
||||
return executePayment(result.payment, key);
|
||||
}
|
||||
|
||||
@@ -104,20 +109,31 @@ async function executePayment(payment, key) {
|
||||
}
|
||||
// Persist before entering native code so a killed process remains recoverable.
|
||||
uni.setStorageSync(key, payment.orderNum);
|
||||
const nativeResult = await new Promise(resolve => openDypay({ payInfo, showLoading: true }, resolve));
|
||||
const state = await query(payment.orderNum);
|
||||
if (state.state === 'pending' && nativeResult?.resultCode && !['0', '3'].includes(String(nativeResult.resultCode))) {
|
||||
// A native error is diagnostic only: never clear a potentially live payment on its authority.
|
||||
const descriptions = { '1': '支付已取消', '2': '支付 SDK 调用失败', '100': '请安装或升级抖音客户端',
|
||||
'103': '原生支付仍在处理中', '-1': '支付参数校验失败' };
|
||||
state.message = `${descriptions[String(nativeResult.resultCode)] || '支付未完成'},请查询订单后重试`;
|
||||
}
|
||||
showState(state, key, payment.orderNum);
|
||||
return state;
|
||||
const session = { key, orderNum: payment.orderNum, settled: false, finish: null };
|
||||
inFlight = session;
|
||||
try {
|
||||
const nativeResult = await new Promise(resolve => {
|
||||
session.finish = result => {
|
||||
if (session.settled) return;
|
||||
session.settled = true;
|
||||
resolve(result);
|
||||
};
|
||||
openDypay({ payInfo, showLoading: true }, session.finish);
|
||||
});
|
||||
const state = nativeResult?.serverState || await query(payment.orderNum);
|
||||
if (state.state === 'pending' && nativeResult?.resultCode && !['0', '3'].includes(String(nativeResult.resultCode))) {
|
||||
// A native error is diagnostic only: never clear a potentially live payment on its authority.
|
||||
const descriptions = { '1': '支付已取消', '2': '支付 SDK 调用失败', '100': '请安装或升级抖音客户端',
|
||||
'103': '原生支付仍在处理中', '-1': '支付参数校验失败' };
|
||||
state.message = `${descriptions[String(nativeResult.resultCode)] || '支付未完成'},请查询订单后重试`;
|
||||
}
|
||||
await showState(state, key, payment.orderNum);
|
||||
return state;
|
||||
} finally { if (inFlight === session) inFlight = null; }
|
||||
// #endif
|
||||
}
|
||||
|
||||
// Runs BEFORE creating an order. A pending record offers explicit continuation, not a new order.
|
||||
// Runs on the payment click BEFORE creating an order. Resume a live original transaction directly.
|
||||
export async function prepareDouyinPay(payway) {
|
||||
if (payway !== 'douyin') return true;
|
||||
try {
|
||||
@@ -134,13 +150,6 @@ export async function prepareDouyinPay(payway) {
|
||||
try {
|
||||
const result = await continuePendingPayment(key, previous);
|
||||
if (result.state !== 'closed' || key !== accountKey() || uni.getStorageSync(key)) return false;
|
||||
const confirmed = await new Promise(resolve => uni.showModal({
|
||||
title: '原支付已关闭',
|
||||
content: '上一笔支付已确认关闭。是否按当前页面重新发起支付?原商品订单若已过期,请返回商品页重新下单。',
|
||||
confirmText: '重新支付', cancelText: '暂不支付',
|
||||
success: res => resolve(res.confirm === true), fail: () => resolve(false)
|
||||
}));
|
||||
if (!confirmed || key !== accountKey() || uni.getStorageSync(key)) return false;
|
||||
if (!canOpenDypay()) throw new Error('请先安装或升级抖音客户端');
|
||||
return true;
|
||||
}
|
||||
@@ -182,6 +191,22 @@ export async function appDypayFun(payment) {
|
||||
|
||||
export async function resumeDouyinPay() {
|
||||
// #ifdef APP-PLUS
|
||||
// The SDK callback may be lost when returning from Douyin. Reconcile onShow even
|
||||
// while native payment is waiting; only a terminal server result can finish it.
|
||||
const session = inFlight;
|
||||
if (session) {
|
||||
if (checkingForeground || session.settled || session.key !== accountKey()) return;
|
||||
checkingForeground = true;
|
||||
try {
|
||||
const state = await query(session.orderNum);
|
||||
if (session === inFlight && !session.settled && session.key === accountKey() && state.state !== 'pending') {
|
||||
resetDypay();
|
||||
session.finish({ serverState: state });
|
||||
}
|
||||
} catch (_) { /* A failed foreground query must not unlock a live transaction. */ }
|
||||
finally { checkingForeground = false; }
|
||||
return;
|
||||
}
|
||||
if (active || resuming) return;
|
||||
const key = accountKey();
|
||||
const pending = key && uni.getStorageSync(key);
|
||||
@@ -190,7 +215,7 @@ export async function resumeDouyinPay() {
|
||||
try {
|
||||
const state = await query(pending);
|
||||
if (key === accountKey() && state.state !== 'pending') resetDypay();
|
||||
showState(state, key, pending);
|
||||
await showState(state, key, pending);
|
||||
}
|
||||
catch (_) { /* Keep the order for a later onShow. Never infer a payment failure. */ }
|
||||
finally { resuming = false; }
|
||||
|
||||
+5
-6
@@ -1,4 +1,5 @@
|
||||
import { appDypayFun } from './douyin-pay.js';
|
||||
import { appDypayFun } from './douyin-pay.js';
|
||||
import { jumpToPaymentSuccess } from './payment-navigation.js';
|
||||
import { feedback } from "@/api/payment.js";
|
||||
|
||||
/**
|
||||
@@ -123,11 +124,9 @@ export const appDyPayFun = douyinPayFun;
|
||||
export const appDouyinPayFun = douyinPayFun;
|
||||
|
||||
// 跳转到支付成功页面
|
||||
export function jumpWayOk() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/order_package/order_submit_ok/order_submit_ok",
|
||||
});
|
||||
}
|
||||
export function jumpWayOk() {
|
||||
return jumpToPaymentSuccess();
|
||||
}
|
||||
// 跳转到支付失败页面
|
||||
export function jumpWayError() {
|
||||
uni.navigateTo({
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Shared result pages for WeChat, Alipay and Douyin. Keep the existing page design.
|
||||
export function jumpToPaymentSuccess() {
|
||||
const url = '/pages/order_package/order_submit_ok/order_submit_ok';
|
||||
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : [];
|
||||
if (pages[pages.length - 1]?.route === url.slice(1)) return Promise.resolve(true);
|
||||
return new Promise(resolve => {
|
||||
uni.navigateTo({ url, success: () => resolve(true), fail: () => {
|
||||
// A full page stack must not leave a confirmed payment on the checkout screen.
|
||||
uni.redirectTo({ url, success: () => resolve(true), fail: () => resolve(false) });
|
||||
} });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user