fix: 隔离抖音当前支付订单与缓存旧单
This commit is contained in:
@@ -19,7 +19,7 @@ function preprocess(source, app) {
|
||||
}
|
||||
function loadModule(file, context, app = true) {
|
||||
const source = preprocess(fs.readFileSync(path.join(root, file), 'utf8'), app)
|
||||
.replace(/^import .*;\r?$/gm, '')
|
||||
.replace(/^import [^\r\n]*;[^\S\r\n]*(?:\/\/[^\r\n]*)?\r?$/gm, '')
|
||||
.replace(/export (async function|function)/g, '$1')
|
||||
.replace(/export const /g, 'const ')
|
||||
.replace(/export default request;/g, '');
|
||||
@@ -167,7 +167,7 @@ test('request terminates on HTTP500, business500, bad JSON, auth failure and off
|
||||
}
|
||||
});
|
||||
|
||||
test('killed App resumes the original unpaid transaction, without creating another order', async () => {
|
||||
test('killed App only resumes the original transaction when the current response selects that same order', async () => {
|
||||
const ctx = await payContext();
|
||||
// This is exactly the legacy persisted format: only an order number survives process death.
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
@@ -182,8 +182,10 @@ test('killed App resumes the original unpaid transaction, without creating anoth
|
||||
};
|
||||
ctx.openDypay = (options, cb) => { opened++; assert.equal(options.payInfo.prepayid, 'prepay'); cb({resultCode:'0'}); };
|
||||
ctx.queryUntilSettled = async () => ({state:'paid'});
|
||||
// Returning false intentionally stops the caller from creating a NEW order after resuming.
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
assert.equal(opened, 0);
|
||||
assert.deepEqual(urls, []);
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(opened, 1);
|
||||
assert.deepEqual(urls, ['/payment/douyin/resume']);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
@@ -199,7 +201,7 @@ test('cold start never opens the SDK or asks for payment and preserves pending s
|
||||
assert.deepEqual(ctx.uni.navigations, [failurePage]);
|
||||
});
|
||||
|
||||
test('unknown, processing, network error and missing backend remain locked', async () => {
|
||||
test('retry of the SAME order remains blocked on unknown, processing, network error or missing backend', async () => {
|
||||
for (const mode of ['pending','NEW_STATE','offline','missing']) {
|
||||
const ctx = await payContext({openDypay:()=>assert.fail('Unknown result must not open SDK'),
|
||||
request: async()=> {
|
||||
@@ -208,7 +210,8 @@ test('unknown, processing, network error and missing backend remain locked', asy
|
||||
return {bizcode:100,data:{state:mode,orderNum:payment.orderNum}};
|
||||
}});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
}
|
||||
});
|
||||
@@ -220,38 +223,41 @@ test('terminal server result clears saved order without payment and allows the n
|
||||
? ({bizcode:100,data:{state,orderNum:payment.orderNum}})
|
||||
: ({bizcode:100,data:{state:2}})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.deepEqual(ctx.uni.navigations, [state === 'paid' ? successPage : failurePage]);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
}
|
||||
});
|
||||
|
||||
test('closed old transaction unlocks a fresh server order, never reopens old parameters', async () => {
|
||||
test('a different current order does not resume or close the cached old transaction', async () => {
|
||||
let resets=0, opened;
|
||||
const ctx=await payContext({resetDypay:()=>resets++, request:async options => options.url === '/payment/douyin/resume'
|
||||
? {bizcode:100,data:{state:'closed',orderNum:'expired-order'}} : {bizcode:100,data:{state:2}},
|
||||
const ctx=await payContext({resetDypay:()=>resets++, request:async options => {
|
||||
assert.equal(options.url, '/payment/queryResult');
|
||||
assert.equal(options.data.orderNum, 'fresh-order');
|
||||
return {bizcode:100,data:{state:2}};
|
||||
},
|
||||
openDypay:(options,cb)=>{opened=options.payInfo.prepayid; cb({resultCode:'0'});}});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','expired-order');
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),false);
|
||||
assert.deepEqual(ctx.uni.navigations,[failurePage]);
|
||||
// A NEW user action may now create a fresh order; the closed attempt cannot do it implicitly.
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),true);
|
||||
assert.equal(resets,1);
|
||||
assert.deepEqual(ctx.uni.navigations,[]);
|
||||
assert.equal(resets,0);
|
||||
const fresh={...payment,orderNum:'fresh-order',douyin:{...payment.douyin,prepayId:'fresh-prepay'}};
|
||||
await ctx.appDypayFun(fresh);
|
||||
assert.equal(resets,1);
|
||||
assert.equal(opened,'fresh-prepay');
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
|
||||
test('switching accounts during old-order closure cannot create a new payment', async () => {
|
||||
test('switching accounts during same-order reconciliation 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);
|
||||
await ctx.appDypayFun({...payment,orderNum:'expired-order'});
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),'expired-order');
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
});
|
||||
@@ -260,7 +266,7 @@ test('unconfirmed server state never resets the native lock and explains why', a
|
||||
const ctx=await payContext({resetDypay:()=>assert.fail('Unconfirmed native reset'),
|
||||
request:async()=>({bizcode:100,data:{state:'pending',reason:'QUERY_FAILED',orderNum:payment.orderNum}})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7',payment.orderNum);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),false);
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.match(ctx.uni.messages.at(-1),/查单失败/);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),payment.orderNum);
|
||||
});
|
||||
@@ -271,7 +277,7 @@ test('unpaid server confirmation resets a missing native callback before reiniti
|
||||
request:async()=>({bizcode:100,data:{state:'unpaid',orderNum:payment.orderNum,payment}}),
|
||||
queryUntilSettled:async()=>({state:'pending'})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7',payment.orderNum);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),false);
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.deepEqual(sequence,['reset','init']);
|
||||
});
|
||||
|
||||
@@ -292,7 +298,7 @@ test('account switch during resume response or native reset cannot invoke SDK',
|
||||
return {bizcode:100,data:{state:'unpaid',orderNum:payment.orderNum,payment}};
|
||||
};
|
||||
if (phase === 'reset') ctx.resetDypay = () => ctx.uni.storage.set('user',{userId:8});
|
||||
await ctx.prepareDouyinPay('douyin');
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
}
|
||||
});
|
||||
@@ -304,7 +310,7 @@ test('concurrent clicks and onShow during continuation open only one native paym
|
||||
queryUntilSettled: async()=>({state:'paid'}),
|
||||
openDypay:(_,cb)=>{ calls++; done = cb; }});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
const first = ctx.prepareDouyinPay('douyin');
|
||||
const first = ctx.appDypayFun(payment);
|
||||
await new Promise(resolve=>setImmediate(resolve));
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
await ctx.appDypayFun(payment);
|
||||
@@ -370,7 +376,7 @@ test('full page stack falls back to redirect; navigation failure remains retryab
|
||||
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');
|
||||
await failed.appDypayFun(payment);
|
||||
assert.deepEqual(failed.uni.navigations,[successPage]);
|
||||
assert.equal(failed.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
@@ -401,7 +407,7 @@ test('resume refuses mismatched order, bad signed parameters and native initiali
|
||||
if (mode === 'init') ctx.initDypay = ()=>false;
|
||||
ctx.request = async()=>({bizcode:100,data:{state:'unpaid',
|
||||
orderNum:mode === 'outer-order' ? 'different' : payment.orderNum,payment:resumePayment}});
|
||||
await ctx.prepareDouyinPay('douyin');
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
}
|
||||
});
|
||||
@@ -479,3 +485,117 @@ test('WeChat and Alipay failures retain the same shared failure page', async ()
|
||||
ctx.zfbPayFun({type:'app'},2,'ali');
|
||||
assert.deepEqual(ctx.uni.navigations,[failurePage,failurePage]);
|
||||
});
|
||||
|
||||
test('cancel 200 yuan then buy 0.2 yuan uses the current checkout response, never the cached 200 yuan order', async () => {
|
||||
const oldOrder = {...payment, orderNum:'order-200', douyin:{...payment.douyin, prepayId:'prepay-200', sign:'signature-200'}};
|
||||
const current = {...payment, orderNum:'order-0.2', douyin:{...payment.douyin, prepayId:'prepay-0.2', sign:'signature-0.2'}};
|
||||
const opened = [], queried = [], requested = [];
|
||||
let lateOldCallback;
|
||||
const ctx = await payContext({
|
||||
request: async options => {
|
||||
requested.push(options.url);
|
||||
assert.equal(options.url, '/cart/settlementOrder');
|
||||
assert.equal(options.data.goodsId, 'product-0.2');
|
||||
return {bizcode:100, data:current};
|
||||
},
|
||||
queryUntilSettled: async (_, orderNum) => { queried.push(orderNum); return {state:orderNum === 'order-200' ? 'pending' : 'paid'}; },
|
||||
openDypay: (options, cb) => {
|
||||
opened.push(options.payInfo);
|
||||
if (options.payInfo.prepayid === 'prepay-200') { lateOldCallback = cb; cb({resultCode:'1'}); }
|
||||
else cb({resultCode:'0'});
|
||||
}
|
||||
});
|
||||
await ctx.appDypayFun(oldOrder);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), 'order-200');
|
||||
assert.deepEqual(ctx.uni.navigations, [failurePage]);
|
||||
// Same sequence as checkout: preflight -> current business API -> SDK.
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
assert.equal(opened.length, 1);
|
||||
loadModule('api/cart.js', ctx);
|
||||
const response = await ctx.settlementOrder({goodsId:'product-0.2',payway:'douyin'});
|
||||
await ctx.appDypayFun(response.data);
|
||||
lateOldCallback({resultCode:'0'});
|
||||
assert.deepEqual(requested, ['/cart/settlementOrder']);
|
||||
assert.deepEqual(opened.map(v=>v.prepayid), ['prepay-200','prepay-0.2']);
|
||||
assert.equal(opened[1].sign, 'signature-0.2');
|
||||
assert.deepEqual(queried, ['order-200','order-0.2']);
|
||||
assert.deepEqual(ctx.uni.navigations, [failurePage,successPage]);
|
||||
});
|
||||
|
||||
test('new purchase, recharge and existing-order payment all ignore an unrelated cached order', async () => {
|
||||
for (const [entry, file, method, url] of [
|
||||
['purchase','api/cart.js','settlementOrder','/cart/settlementOrder'],
|
||||
['recharge','api/finance.js','recharge','/finance/recharge'],
|
||||
['existing-order','api/order.js','payment','/order/payment']
|
||||
]) {
|
||||
const current={...payment,orderNum:entry,douyin:{...payment.douyin,prepayId:entry}};
|
||||
const ctx = await payContext({request:async options=>{
|
||||
assert.equal(options.url,url);
|
||||
assert.equal(options.data.payway,'douyin');
|
||||
return {bizcode:100,data:current};
|
||||
},
|
||||
queryUntilSettled:async(_, orderNum)=>{assert.equal(orderNum, entry);return {state:'pending'};}});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', 'old-200');
|
||||
let received;
|
||||
ctx.openDypay = (options, cb)=>{received=options.payInfo;cb({resultCode:'1'});};
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), 'old-200');
|
||||
loadModule(file,ctx);
|
||||
const response=await ctx[method]({payway:'douyin'});
|
||||
await ctx.appDypayFun(response.data);
|
||||
assert.equal(received.prepayid, entry);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), entry);
|
||||
}
|
||||
});
|
||||
|
||||
test('legacy entry also pays the supplied order rather than a cached order', async () => {
|
||||
const ctx=await payContext({request:()=>assert.fail('Unrelated resume'), queryUntilSettled:async()=>({state:'paid'})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','old-200');
|
||||
loadModule('utils/payUtils.js',ctx);
|
||||
let opened;
|
||||
ctx.openDypay=(options,cb)=>{opened=options.payInfo.prepayid;cb({resultCode:'0'});};
|
||||
await ctx.douyinPayFun({...payment.douyin,prepayId:'current-0.2'},2,'current-order');
|
||||
assert.equal(opened,'current-0.2');
|
||||
});
|
||||
|
||||
test('bad current parameters never fall back to an unrelated old order or reset its native session', async () => {
|
||||
const ctx=await payContext({request:()=>assert.fail('Old-order fallback'),
|
||||
resetDypay:()=>assert.fail('Invalid response must not reset native'),openDypay:()=>assert.fail('Invalid payment')});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','old-200');
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),true);
|
||||
assert.equal((await ctx.appDypayFun({orderNum:'new',douyin:{appId:'app'}})).state,'error');
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),'old-200');
|
||||
});
|
||||
|
||||
test('restart can reconcile the last attempt but a later new checkout cannot reopen it', async () => {
|
||||
const ctx=await payContext({request:()=>assert.fail('Old resume'),queryUntilSettled:async()=>({state:'pending'})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','old-200');
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),true);
|
||||
let opened;
|
||||
ctx.openDypay=(options,cb)=>{opened=options.payInfo.prepayid;cb({resultCode:'1'});};
|
||||
await ctx.appDypayFun({...payment,orderNum:'new-0.2',douyin:{...payment.douyin,prepayId:'new-0.2'}});
|
||||
assert.equal(opened,'new-0.2');
|
||||
});
|
||||
|
||||
test('an actually running SDK payment still blocks a second order until its callback completes', async () => {
|
||||
let callback, calls=0;
|
||||
const ctx=await payContext({openDypay:(_,cb)=>{calls++;callback=cb;},queryUntilSettled:async()=>({state:'pending'})});
|
||||
const first=ctx.appDypayFun(payment);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),false);
|
||||
await ctx.appDypayFun({...payment,orderNum:'different'});
|
||||
assert.equal(calls,1);
|
||||
callback({resultCode:'1'});
|
||||
await first;
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),true);
|
||||
});
|
||||
|
||||
test('native reset for a different order cannot pay after an account switch', async () => {
|
||||
const ctx=await payContext({openDypay:()=>assert.fail('Wrong account'),
|
||||
resetDypay:()=>ctx.uni.storage.set('user',{userId:8})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','old-200');
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),'old-200');
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:8'),false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user