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
+71 -2
View File
@@ -118,13 +118,82 @@ test('signed Taobao identities, replay defense, H5 isolation, shared report cont
const csv = await (await fetch(base + '/api/admin/export?channel=taobao')).text();
const csvLines = csv.trim().split('\n');
assert.equal(csvLines.length, 2); assert.equal(csvLines[0].split(',')[2], '年龄', 'existing CSV column positions remain stable');
assert.equal(csvLines[0].split(',').at(-1).trim(), '渠道'); assert.equal(csvLines[1].split(',').at(-1).trim(), 'taobao');
const channelColumn = csvLines[0].split(',').findIndex(value => value.trim() === '渠道');
assert(channelColumn >= 0); assert.equal(csvLines[1].split(',')[channelColumn].trim(), 'taobao');
const oldAccounts = require('../../h5/lib/accounts')(path.join(dir, 'accounts'), {});
assert.equal(oldAccounts.taobaoSession({ headers: { authorization: 'Bearer ' + alice.token } }).id, alice.user.id, 'restart reloads account/session');
assert.equal(oldAccounts.reports(alice.user.id).length, 1);
await post('/api/taobao/logout', {}, bob.token);
assert.equal((await post('/api/taobao/me', {}, bob.token)).data.data.http_status, 401);
});
test('session restore binds a cached account Bearer to the current signed Taobao host identity', async () => {
const a = await login('host-account-a'), b = await login('host-account-b');
assert.equal((await post('/api/taobao/me', {}, a.token)).data.data.http_status, 401, 'unsigned restore cannot silently trust an old Bearer');
assert.equal((await post('/api/taobao/me', {}, a.token, a.params)).data.data.user.id, a.user.id);
assert.equal((await post('/api/taobao/me', {}, a.token, b.params)).data.data.http_status, 401, 'switching the Taobao host account rejects the prior identity');
assert.equal((await post('/api/taobao/me', {}, a.token, { app_key: env.TAOBAO_APP_KEY })).data.data.http_status, 401, 'permanent identity cannot restore without open_id');
assert.equal((await post('/api/taobao/me', {}, a.token, a.params)).data.data.user.id, a.user.id, 'one host switch does not revoke valid sessions on other devices');
});
test('signed Taobao guests can complete one private analysis without identity, photo, result or report persistence', async () => {
function diskSnapshot() {
return fs.readdirSync(dir, { recursive: true, withFileTypes: true }).filter(item => item.isFile() && item.name !== 'provider-count')
.map(item => { const file = path.join(item.parentPath || item.path, item.name); return [path.relative(dir, file), require('node:crypto').createHash('sha256').update(fs.readFileSync(file)).digest('hex')]; })
.sort((a, b) => a[0].localeCompare(b[0]));
}
const diskBefore = diskSnapshot();
const gateway = { app_key: env.TAOBAO_APP_KEY, env: 'test' };
const challenge = (await post('/api/taobao/challenge')).data.data.challenge;
const body = { consent: true, challenge, open_id: 'client-forged', kind: 'taobao', id: 'client-id' };
assert.equal((await post('/api/taobao/guest-session', body)).data.data.http_status, 401);
assert.equal((await post('/api/taobao/guest-session', body, null, { ...gateway, app_key: 'wrong-app' })).data.data.http_status, 401);
assert.equal((await post('/api/taobao/session', body, null, gateway)).data.data.http_status, 401, 'anonymous gateway is not a permanent account login');
assert.equal((await post('/api/taobao/guest-session', { ...body, consent: false }, null, gateway)).data.data.http_status, 400);
const guest = (await post('/api/taobao/guest-session', body, null, gateway)).data.data;
assert.equal(guest.user.kind, 'taobao_guest'); assert.notEqual(guest.user.id, body.id);
assert.equal(guest.expires_in, 8 * 3600);
assert.equal((await post('/api/taobao/guest-session', body, null, gateway)).data.data.http_status, 401, 'guest challenges cannot replay');
const secondChallenge = (await post('/api/taobao/challenge')).data.data.challenge;
const other = (await post('/api/taobao/guest-session', { consent: true, challenge: secondChallenge }, null, gateway)).data.data;
assert.notEqual(other.user.id, guest.user.id, 'visits never reuse a platform identity');
assert.equal((await post('/api/taobao/me', {}, guest.token)).data.data.http_status, 401);
assert.equal((await post('/api/taobao/me', {}, guest.token, gateway)).data.data.user.kind, 'taobao_guest', 'signed guest restore does not require account authorization or open_id');
const asCookie = await (await fetch(base + '/api/auth/session', { headers: { cookie: 'juzi_session=' + guest.token } })).json();
assert.equal(asCookie.user, null, 'miniapp guest Bearers cannot become H5 cookie sessions');
const refs = await upload(guest.token), profile = { real_age: 35, gender: '女' }, requestId = 'guest-analysis-attempt';
const count = () => fs.readFileSync(path.join(dir, 'provider-count'), 'utf8').trim().split('\n').length;
const beforeCalls = count();
const submission = { photo_refs: refs, profile, request_id: requestId };
const duplicates = await Promise.all([post('/api/taobao/skin-analyze', submission, guest.token), post('/api/taobao/skin-analyze', submission, guest.token)]);
const first = duplicates[0].data.data, repeat = duplicates[1].data.data;
assert.equal(first.http_status, 202); assert.equal(repeat.http_status, 202); assert.equal(first.job, repeat.job);
assert.equal((await post('/api/taobao/analysis', { job: first.job }, other.token)).data.data.http_status, 404);
assert.equal((await post('/api/taobao/skin-analyze', submission, other.token)).data.data.http_status, 409, 'another visitor cannot retrieve the idempotent job');
const report = await poll(guest.token, first.job);
assert.equal(report.ok, true); assert.equal(report.report_saved, false);
assert.equal(report.presentation.views.length, 7);
assert.equal(count() - beforeCalls, 1, 'duplicate starts cause one paid primary request');
assert.equal((await post('/api/taobao/skin-analyze', submission, guest.token)).data.data.job, first.job);
assert.equal((await post('/api/taobao/skin-analyze', { ...submission, profile: { real_age: 36 } }, guest.token)).data.data.http_status, 409);
const history = (await post('/api/taobao/reports', {}, guest.token)).data.data;
assert.equal(history.total, 0); assert.deepEqual(history.items, []);
assert.equal((await post('/api/taobao/reports', { id: report.record_id }, guest.token)).data.data.http_status, 404);
const side = (await post('/api/taobao/wrinkle-views', { token: report.wrinkle_job.token }, guest.token)).data.data;
assert.equal((await poll(guest.token, side.job)).ok, true);
const afterSideCalls = count();
const cachedSide = (await post('/api/taobao/wrinkle-views', { token: report.wrinkle_job.token }, guest.token)).data.data;
assert.equal((await poll(guest.token, cachedSide.job)).ok, true);
assert.equal(count(), afterSideCalls, 'guest side layers also reuse a finished provider job');
assert.deepEqual(diskSnapshot(), diskBefore, 'guest lifecycle writes no accounts, records, photos, results, report or side-job files');
const reloaded = require('../../h5/lib/accounts')(path.join(dir, 'accounts'), {});
assert.equal(reloaded.taobaoSession({ headers: { authorization: 'Bearer ' + guest.token } }), null, 'process restart cannot restore a guest identity');
await post('/api/taobao/logout', {}, guest.token);
assert.equal((await post('/api/taobao/analysis', { job: first.job }, guest.token)).data.data.http_status, 401);
assert.equal((await post('/api/taobao/me', {}, guest.token)).data.data.http_status, 401);
await post('/api/taobao/logout', {}, other.token);
assert.deepEqual(diskSnapshot(), diskBefore, 'guest logout also avoids persistence');
});
test('native API client logs in, uploads within gateway limit, polls one detection, restores identity and honors cancellation', async () => {
const config = require('../config'), storage = new Map();
config.baseUrl = base; config.transport = 'direct'; config.basePath = '';
@@ -136,7 +205,7 @@ test('native API client logs in, uploads within gateway limit, polls one detecti
request(options) {
requests++; assert(Buffer.byteLength(options.data) < 128 * 1024);
const url = new URL(options.url);
if (url.pathname === '/api/taobao/session') {
if (url.pathname === '/api/taobao/session' || url.pathname === '/api/taobao/me') {
const params = { app_key: env.TAOBAO_APP_KEY, open_id: 'native-client-user', sign_method: 'md5' };
params.sign = auth.signature(params, options.data, env.TAOBAO_APP_SECRET);
url.search = new URLSearchParams(params).toString();