feat: 完善淘宝小程序拍照、报告与登录流程

接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
This commit is contained in:
xsl
2026-11-04 15:07:51 +08:00
parent 381da60ae4
commit 774376fa16
147 changed files with 17420 additions and 597 deletions
+117
View File
@@ -13,6 +13,21 @@ test('gateway signature covers raw JSON, identity, extra query fields and algori
assert.equal(auth.verify(url + '&open_id=open-account', body, env), null);
assert.equal(auth.verify(url, body, {}), null);
});
test('guest gateway verification permits missing open_id but still requires app identity and an intact signature', () => {
const env = { TAOBAO_APP_KEY: 'fixture-app', TAOBAO_APP_SECRET: 'fixture-secret' };
const body = '{"consent":true,"challenge":"fixture-challenge"}';
const params = { app_key: env.TAOBAO_APP_KEY, env: 'test', request_id: 'gateway-request' };
const signed = values => '/api/taobao/guest-session?' + new URLSearchParams({ ...values, sign: auth.signature(values, body, env.TAOBAO_APP_SECRET) });
const url = signed(params);
assert.equal(auth.verifyGateway(url, body, env).appKey, env.TAOBAO_APP_KEY);
assert.equal(auth.verify(url, body, env), null, 'normal login still requires a signed open_id');
assert.equal(auth.verifyGateway('/api/taobao/guest-session', body, env), null);
assert.equal(auth.verifyGateway(url, body + ' ', env), null);
assert.equal(auth.verifyGateway(signed({ ...params, app_key: 'other-app' }), body, env), null);
assert.equal(auth.verifyGateway(url + '&env=test', body, env), null);
assert.equal(auth.verifyGateway(signed({ ...params, sign_method: 'none' }), body, env), null);
});
test('photo chunks are ordered, bounded, deduplicated, owner-bound, expiring and consumed once', async () => {
let now = 0;
const req = { headers: {}, owner: 'alice' }, other = { headers: {}, owner: 'bob' };
@@ -39,3 +54,105 @@ test('photo chunks are ordered, bounded, deduplicated, owner-bound, expiring and
now = 5 * 60000 + 1; assert.equal(runtime.takePhotos(req, refs), null);
const challenge = runtime.challenge('local'); assert(runtime.consume(challenge)); assert(!runtime.consume(challenge));
});
test('analysis attempts are session scoped, deduplicated, content bound and never restarted after result expiry', async () => {
let now = 0, charged = 0;
const runtime = require('../../h5/lib/taobao-session')({ now: () => now, session: () => ({ id: 'same-account', kind: 'taobao' }) });
const alice = { headers: { authorization: 'Bearer fixture-session-a' } }, bob = { headers: { authorization: 'Bearer fixture-session-b' } };
const body = { photo_refs: { front: 'front-upload', left: 'left-upload', right: 'right-upload' }, profile: { real_age: 30, gender: '女' } };
const requestId = 'analysis-attempt-1';
for (const req of [alice, bob]) {
for (const view of ['front', 'left', 'right']) assert.equal(runtime.upload(req, { view, upload_id: body.photo_refs[view], index: 0, total: 1, data: 'AAAA' }).status, 200);
}
assert(runtime.takePhotos(alice, body.photo_refs));
assert(runtime.takePhotos(bob, body.photo_refs), 'same account on two devices cannot consume each other\'s uploads');
const attempt = runtime.submission(alice, requestId, body);
assert.equal(attempt.status, 200);
const job = runtime.start(alice, async () => { charged++; return { ok: true }; }, true, attempt);
assert.equal(runtime.result(bob, job), null, 'same-account Bearers cannot read each other\'s transient jobs');
assert.deepEqual(runtime.submission(alice, requestId, body), { status: 202, job });
const reordered = { profile: { gender: '女', real_age: 30 }, photo_refs: { right: 'right-upload', front: 'front-upload', left: 'left-upload' } };
assert.equal(runtime.submission(alice, requestId, reordered).job, job, 'JSON property order does not change the request identity');
assert.equal(runtime.submission(alice, requestId, { ...body, profile: { real_age: 31 } }).status, 409);
assert.equal(runtime.submission(alice, '', body).status, 400);
assert.equal(runtime.submission(alice, undefined, body).status, 200, 'older callers remain supported');
const second = runtime.submission(bob, requestId, body);
assert.equal(second.status, 200, 'an id in another Bearer session never exposes a prior job');
const otherJob = runtime.start(bob, async () => { charged++; return { ok: true }; }, true, second);
assert.notEqual(otherJob, job);
await new Promise(resolve => setImmediate(resolve));
assert.equal(charged, 2);
assert.equal(runtime.submission(alice, requestId, body).job, job, 'completed attempts still return the first job');
now = 5 * 60000 + 1;
assert.equal(runtime.result(alice, job), null);
assert.equal(runtime.submission(alice, requestId, body).status, 410, 'expired results retain a non-recharge tombstone');
assert.equal(charged, 2);
runtime.revoke(bob);
assert.equal(runtime.result(bob, otherJob), null);
});
test('a full analysis queue preserves uploaded photos and the same attempt can retry exactly once after capacity returns', async () => {
let charged = 0;
const runtime = require('../../h5/lib/taobao-session')({ session: () => ({ id: 'account', kind: 'taobao' }) });
const client = { headers: { authorization: 'Bearer upload-session' } };
const busy = { headers: { authorization: 'Bearer busy-session' } };
for (let i = 0; i < 1000; i++) assert(runtime.start(busy, () => new Promise(() => {})));
const refs = {};
for (const view of ['front', 'left', 'right']) {
refs[view] = view + '-capacity-upload';
assert.equal(runtime.upload(client, { view, upload_id: refs[view], index: 0, total: 1, data: 'AAAA' }).status, 200);
}
const body = { request_id: 'capacity-retry-attempt', photo_refs: refs, profile: { real_age: 30 } };
const run = async photos => { charged++; assert.equal(photos.front_image, 'AAAA'); return { ok: true }; };
assert.equal(runtime.beginAnalysis(client, body, run).status, 429);
assert.equal(charged, 0);
runtime.revoke(busy);
const accepted = runtime.beginAnalysis(client, body, run);
assert.equal(accepted.status, 202, 'retry succeeds with the original uploaded refs');
assert.deepEqual(runtime.beginAnalysis(client, body, run), accepted, 'duplicate delivery returns the reserved job');
await new Promise(resolve => setImmediate(resolve));
assert.equal(charged, 1);
assert.equal(runtime.result(client, accepted.job).payload.ok, true);
assert.deepEqual(runtime.beginAnalysis(client, body, run), accepted, 'completion also retains idempotency');
assert.equal(charged, 1);
});
test('login challenges expire, are consumed once and are rate limited', () => {
let now = 0;
const runtime = require('../../h5/lib/taobao-session')({ now: () => now, session: () => null });
const expired = runtime.challenge('address-a');
now = 60001;
assert.equal(runtime.consume(expired), false);
const once = runtime.challenge('address-a');
assert.equal(runtime.consume(once), true);
assert.equal(runtime.consume(once), false);
for (let i = 0; i < 29; i++) assert(runtime.challenge('address-a'));
assert.equal(runtime.challenge('address-a'), null);
now += 60001;
assert(runtime.challenge('address-a'));
});
test('miniapp guest Bearers expire, revoke and disappear on reload without entering the account database', t => {
const fs = require('node:fs'), path = require('node:path');
const tempRoot = path.resolve(__dirname, '../../temp');
const dir = fs.mkdtempSync(path.join(tempRoot, 'guest-account-test-'));
t.after(() => { assert(dir.startsWith(tempRoot + path.sep)); fs.rmSync(dir, { recursive: true, force: true }); });
let now = Date.now();
t.mock.method(Date, 'now', () => now);
const createAccounts = require('../../h5/lib/accounts'), accounts = createAccounts(dir, {});
const guest = accounts.loginTaobaoGuest();
const req = { headers: { authorization: 'Bearer ' + guest.token } };
assert.equal(accounts.taobaoSession(req).kind, 'taobao_guest');
assert.equal(fs.existsSync(path.join(dir, 'identities.json')), false);
assert.throws(() => accounts.storeReport(guest.user.id, { id: 'Rguest', vm: {} }), /Invalid report owner/);
assert.equal(createAccounts(dir, {}).taobaoSession(req), null, 'new account store never reloads guest credentials');
now += 8 * 3600000 + 1;
assert.equal(accounts.taobaoSession(req), null);
const saved = accounts.loginTaobao('fixture-provider-key');
const dbBeforeGuest = fs.readFileSync(path.join(dir, 'identities.json'), 'utf8');
const visit = accounts.loginTaobaoGuest(), visitReq = { headers: { authorization: 'Bearer ' + visit.token } };
accounts.taobaoLogout(visitReq);
assert.equal(accounts.taobaoSession(visitReq), null);
assert.equal(accounts.taobaoSession({ headers: { authorization: 'Bearer ' + saved.token } }).kind, 'taobao');
assert.equal(fs.readFileSync(path.join(dir, 'identities.json'), 'utf8'), dbBeforeGuest, 'guest creation and logout never rewrite permanent identities');
});