fix: 移除抖音支付额外弹窗并统一成功页跳转

This commit is contained in:
Codex
2026-09-09 17:30:00 +08:00
parent 2e9a732f9d
commit b5b6359d8e
5 changed files with 197 additions and 66 deletions
+108 -27
View File
@@ -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 () => {