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
+313 -6
View File
@@ -1,10 +1,317 @@
// Standalone device probe. No account API, upload, server call or saved photo.
var AR = require('../../utils/ar-camera');
var image = require('../../utils/image');
var permissions = require('../../utils/permissions');
var config = require('../../config');
var PROBE_VERSION = 'AR-DIAG-20261102-05';
var CALIBRATION_FORMATS = ['RGBA', 'BGRA', 'ARGB', 'ABGR'];
function confirmedPixelFormat() {
try {
var format = image.confirmedFramePixelFormat();
return CALIBRATION_FORMATS.indexOf(format) >= 0 ? format : '';
} catch (_) { return ''; }
}
Page({
data: {},
data: { running: false, busy: false, exporting: false, devicePosition: config.arFrontPosition === 'back' ? 'back' : 'front', cameraSession: 0, status: '请点击开启,验证淘宝真实 AR 帧与照片导出。',
probeVersion: PROBE_VERSION, toolbarTop: 112, environmentText: '', capabilityText: '', contextText: '等待 onInit 后读取公开方法名', eventText: '尚未收到原生事件', copyStatus: '', diagnosticText: '',
target: 'front', frameCount: 0, standardFrameCount: 0, frameText: '尚未收到帧事件',
calibrationItems: [], calibrationReady: false, calibrationText: '', calibrationConfirmed: '',
poseText: '', fieldText: '', errorText: '', snapshotUrl: '', snapshotText: '', compressWidth: 1, compressHeight: 1 },
onLoad: function () {
this.alive = true;
this.alive = true; this.visible = true; this.sequence = 0;
var capability = AR.capability(my);
this.diagnosticSystem = AR.diagnostics.system(my); this.contextMethodNames = []; this.nativeEventStats = {};
this.diagnosticCapability = 'ar-camera=' + capability.componentSignal + '; createArCameraContext=' + capability.contextFactory;
this.setData({ toolbarTop: AR.diagnostics.toolbarTop(my), calibrationConfirmed: confirmedPixelFormat(), environmentText: this.diagnosticSystem.join('\n') || '系统未返回可读取的设备/宿主版本;请手动补充淘宝版本。',
capabilityText: 'ar-camera 声明:' + capability.componentSignal + ';上下文工厂:' + (capability.contextFactory ? '存在' : '不存在') + '。声明结果不代表真机已支持。' });
},
onUnload: function () { this.alive = false; },
onError: function (err) {
my.showToast({ content: '相机组件报错: ' + JSON.stringify(err || {}), type: 'none', duration: 6000 });
}
onShow: function () { this.visible = true; },
flushFrameDiagnostic: function () {
var pending = this.pendingFrameDiagnostic;
if (!pending) return;
this.pendingFrameDiagnostic = null;
var record = this.nativeEventStats[pending.key], summary = AR.diagnostics.event(pending.event);
record.type = summary.type || '(未提供 type)'; record.metadata = summary.metadata;
this.lastDiagnosticMetadata = summary.metadata;
},
nativeEventView: function () {
var that = this;
return { eventText: Object.keys(this.nativeEventStats).map(function (item) {
var value = that.nativeEventStats[item];
return item + ' · type=' + value.type + ' · 次数 ' + value.count;
}).join('\n'), fieldText: (this.lastDiagnosticMetadata || []).join('\n') || '本事件未返回白名单元信息;不会输出未知字段值或人脸坐标。' };
},
recordNativeEvent: function (name, event, deferFrame) {
var key = name + ':request=' + this.data.devicePosition;
var record = this.nativeEventStats[key] || { count: 0, type: '(未提供 type)', metadata: [] };
record.count++;
this.nativeEventStats[key] = record;
// Count every frame in JS, but do not scan descriptors or cross the UI
// bridge until the 400ms refresh. Only the latest frame is retained.
if (deferFrame) { this.pendingFrameDiagnostic = { key: key, event: event }; return; }
this.flushFrameDiagnostic();
var summary = AR.diagnostics.event(event);
record.type = summary.type || '(未提供 type)'; record.metadata = summary.metadata;
this.lastDiagnosticMetadata = summary.metadata;
this.setData(this.nativeEventView());
return summary;
},
diagnosticReport: function () {
// A user-requested copy gets up-to-date counters and last-frame metadata
// without introducing another UI refresh on the high-rate native stream.
this.flushFrameDiagnostic();
var that = this, lines = [PROBE_VERSION].concat(this.diagnosticSystem || []);
lines.push(this.diagnosticCapability || 'capability=unavailable');
lines.push('requestedPosition=' + this.data.devicePosition + '; actualPosition=需核对实际画面');
lines.push('context.methods=' + ((this.contextMethodNames || []).join(', ') || '(none observed)'));
lines.push('pixelFormat.default=' + (CALIBRATION_FORMATS.indexOf(config.arPixelFormat) >= 0 ? config.arPixelFormat : '(none)') + '; defaultIsUserConfirmed=false');
lines.push('pixelFormat.confirmed=' + (confirmedPixelFormat() || '(unconfirmed)'));
lines.push('currentSession.frameEvents=' + (this.frames || 0) + '; parsableFrames=' + (this.standardFrames || 0));
Object.keys(this.nativeEventStats || {}).forEach(function (key) {
var record = that.nativeEventStats[key];
lines.push(key + '; type=' + record.type + '; count=' + record.count);
record.metadata.forEach(function (item) { lines.push(' ' + item); });
});
return lines.join('\n');
},
onCopyDiagnostics: function () {
if (!this.alive) return;
// Always reveal the same privacy-filtered text first, so clipboard failure
// or an absent API still leaves a native selectable-text fallback.
var report = this.diagnosticReport();
this.setData({ diagnosticText: report, copyStatus: '安全诊断已生成;下方文字可长按复制。' });
if (typeof my.setClipboard !== 'function') { this.setData({ copyStatus: '当前淘宝未提供剪贴板 API,请长按下方诊断文字复制。' }); return; }
var that = this;
try {
my.setClipboard({ text: report, success: function () { if (that.alive) that.setData({ copyStatus: '诊断信息已复制。也可长按下方文字复制。' }); },
fail: function () { if (that.alive) that.setData({ copyStatus: '自动复制失败,请长按下方诊断文字复制。' }); } });
} catch (_) { this.setData({ copyStatus: '自动复制失败,请长按下方诊断文字复制。' }); }
},
onOpenNativeProbe: function () {
if (this.data.busy || this.data.exporting) return;
my.redirectTo({ url: '/pages/camnative/camnative' });
},
currentCalibration: function (batch) {
return !!(batch && this.calibrationBatch === batch && batch.sequence === this.sequence && this.alive && this.visible && this.data.running);
},
invalidateCalibration: function () {
if (this.calibrationBatch) this.calibrationBatch.frame = null;
this.calibrationBatch = null;
if (this.alive) this.setData({ calibrationItems: [], calibrationReady: false, calibrationText: '' });
},
onCalibrate: function () {
if (!this.alive || !this.visible || !this.data.running) return Promise.resolve();
if (this.exportInFlight || this.data.busy) {
this.setData({ errorText: '上一批照片仍在导出,请稍候再校验颜色。' }); return Promise.resolve();
}
if (!this.lastFrame) { this.setData({ errorText: '尚未收到真实相机帧,无法校验照片颜色。' }); return Promise.resolve(); }
var frozen, formats;
try {
// Freeze once: every preview and the eventual explicit confirmation refer
// to the exact same image, not a later native/reused camera buffer.
frozen = image.freezeCalibrationFrame(this.lastFrame);
formats = image.frameCalibrationFormats(frozen).filter(function (format) { return CALIBRATION_FORMATS.indexOf(format) >= 0; });
} catch (error) { this.setData({ errorText: error.message || '当前帧无法进行本地颜色校验。' }); return Promise.resolve(); }
if (!formats.length) {
this.setData({ errorText: '当前帧不是可校验的无格式四通道图像,请等待新帧或直接导出照片。' }); return Promise.resolve();
}
this.invalidateCalibration();
var that = this, operation = {}, batch = { sequence: this.sequence, id: this.sequence + '-' + ((this.calibrationSerial || 0) + 1),
frame: frozen, formats: formats, items: [], settled: false };
this.calibrationSerial = (this.calibrationSerial || 0) + 1;
this.calibrationBatch = batch; this.exportInFlight = operation;
this.setData({ busy: true, exporting: true, errorText: '', snapshotUrl: '', snapshotText: '',
calibrationText: '正在按顺序生成本地颜色预览;全部完成后,请选择颜色正常的一张。' });
return formats.reduce(function (chain, format) {
return chain.then(function () {
if (!that.currentCalibration(batch)) return;
return image.captureFrameToBase64(batch.frame, that, { pixelFormat: format, maxSide: 480 }).then(function (url) {
if (!that.currentCalibration(batch)) return;
batch.items.push({ format: format, url: url, batchId: batch.id });
that.setData({ calibrationItems: batch.items.slice(), calibrationText: '已生成 ' + batch.items.length + '/' + batch.formats.length + ' 张,完成后可确认。' });
});
});
}, Promise.resolve()).then(function () {
batch.settled = true;
if (that.currentCalibration(batch)) that.setData({ calibrationReady: true, calibrationText: '请比较肤色和背景,点击颜色正常照片下方的按钮;尚未确认任何新格式。' });
}).catch(function (error) {
batch.settled = true;
if (that.currentCalibration(batch)) {
that.invalidateCalibration();
that.setData({ errorText: error.message || '颜色预览导出失败,请重试。', calibrationText: '本批颜色预览导出失败,未确认新格式,请重试。' });
}
}).then(function () {
if (!that.currentCalibration(batch)) batch.frame = null;
if (that.exportInFlight !== operation) return;
that.exportInFlight = null;
if (that.alive) {
var patch = { exporting: false };
if (batch.sequence === that.sequence) patch.busy = false;
that.setData(patch);
}
});
},
onConfirmCalibration: function (event) {
var batch = this.calibrationBatch, dataset = event && event.currentTarget && event.currentTarget.dataset || {}, format = dataset.format;
if (!this.currentCalibration(batch) || !batch.settled || String(dataset.batch) !== batch.id || batch.formats.indexOf(format) < 0
|| !batch.items.some(function (item) { return item.format === format; })) return;
try {
var confirmed = image.confirmFramePixelFormat(batch.frame, format);
if (confirmed !== format) throw new Error('未能确认相机格式,请重新校验。');
this.invalidateCalibration();
this.setData({ calibrationConfirmed: confirmed, calibrationText: '本次相机格式已确认:' + confirmed + ',已记录本次颜色选择。', errorText: '' });
} catch (error) { this.setData({ errorText: error.message || '格式确认失败,请重新校验。' }); }
},
acceptsCameraEvent: function (event) {
if (!this.alive || !this.visible || !this.data.running) return false;
var target = event && (event.currentTarget || event.target), dataset = target && target.dataset;
// Native callbacks from an unmounted camera must not enter a new session.
// Events without a marker remain compatible with older host callbacks.
if (dataset && dataset.session !== undefined && String(dataset.session) !== String(this.data.cameraSession)) return false;
var id = target && target.id;
return typeof id !== 'string' || id.indexOf('test-ar-camera-') !== 0 || id === 'test-ar-camera-' + this.data.cameraSession;
},
mountCamera: function (sequence) {
if (!this.alive || !this.visible || sequence !== this.sequence) return;
var that = this;
clearTimeout(this.frameTimer);
this.frames = 0; this.standardFrames = 0; this.lastFrame = null; this.context = null; this.lastUpdate = 0; this.frameUiShown = false; this.pendingFrameDiagnostic = null; this.contextMethodNames = [];
this.setData({ running: true, busy: false, cameraSession: sequence, frameCount: 0, standardFrameCount: 0, frameText: '尚未收到帧事件', contextText: '等待 onInit 后读取公开方法名',
status: '已请求' + (this.data.devicePosition === 'front' ? '前置' : '后置') + '镜头,等待真实帧;请核对实际画面…' });
this.frameTimer = setTimeout(function () {
if (that.alive && that.visible && sequence === that.sequence && !that.frames) that.setData({ status: '10秒内未收到帧事件(onARFrame)。请复制诊断,不能视为能力已通过。' });
}, 10000);
},
onStart: function () {
if (this.data.running || this.data.busy) return Promise.resolve();
var that = this, sequence = ++this.sequence;
this.setData({ busy: true, errorText: '', snapshotUrl: '', snapshotText: '', frameCount: 0, poseText: '', fieldText: '' });
// Camera permission only. Neither Taobao userInfo nor album is needed.
return permissions.ensure('scope.camera').then(function () {
if (!that.alive || !that.visible || sequence !== that.sequence) return;
that.mountCamera(sequence);
}).catch(function (error) {
if (that.alive && sequence === that.sequence) that.setData({ errorText: error.message || '相机授权失败' });
}).then(function () { if (that.alive && sequence === that.sequence) that.setData({ busy: false }); });
},
onARInit: function (event) {
if (!this.acceptsCameraEvent(event)) return;
this.recordNativeEvent('onInit', event);
try {
this.context = AR.context(my, 'test-ar-camera-' + this.data.cameraSession);
// Enumerate descriptors only. This diagnostic never probes unknown methods
// or executes property getters/automatic context actions.
this.contextMethodNames = AR.diagnostics.methods(this.context);
this.setData({ contextText: this.contextMethodNames.join(', ') || '未发现可读取的公开方法;不代表原生能力不存在。', status: 'AR 初始化完成,等待实时人脸数据…' });
} catch (error) { this.setData({ errorText: error.message || 'AR 上下文初始化失败' }); }
},
onARFrame: function (event) {
if (!this.acceptsCameraEvent(event)) return;
this.frames = (this.frames || 0) + 1; this.lastFrame = event && event.detail || event;
this.recordNativeEvent('onARFrame', event, true);
clearTimeout(this.frameTimer);
// Facing selects hardware, not the native landmark coordinate convention.
// Do not reverse yaw solely because a front camera was requested.
var verdict;
try { verdict = AR.evaluate(event, this.data.target, false); } catch (_) { verdict = { frameReceived: false }; }
if (verdict.frameReceived) this.standardFrames = (this.standardFrames || 0) + 1;
var now = Date.now();
if (this.frameUiShown && now - this.lastUpdate < 400) return;
this.lastUpdate = now; this.frameUiShown = true;
this.flushFrameDiagnostic();
var patch = this.nativeEventView();
patch.frameCount = this.frames; patch.standardFrameCount = this.standardFrames || 0;
if (!verdict.frameReceived) {
patch.status = '已收到 onARFrame 回调,但格式不符合 width/height/anchors;请复制诊断。';
patch.frameText = '已收到原生回调,尚无可解析的标准帧'; patch.poseText = '';
this.setData(patch);
return;
}
var observation = verdict.observation;
patch.status = verdict.ok ? '实时人脸判断通过;仍需点击导出照片验证原始图像。' : verdict.hint;
patch.frameText = verdict.width + '×' + verdict.height + ' · 人脸 ' + verdict.faceCount + ' · 已收到真实帧';
patch.poseText = observation ? '几何姿态 yaw ' + observation.yaw.toFixed(1) + '° / pitch ' + observation.pitch.toFixed(1) + '° / roll ' + observation.roll.toFixed(1) + '°' : '';
this.setData(patch);
},
onTarget: function (event) {
var target = event.currentTarget.dataset.target;
if (['front', 'left', 'right'].indexOf(target) >= 0) this.setData({ target: target });
},
onFlip: function () {
if (!this.alive || !this.visible || !this.data.running) return Promise.resolve();
var that = this, sequence = ++this.sequence;
clearTimeout(this.frameTimer);
this.flushFrameDiagnostic();
this.invalidateCalibration();
this.lastFrame = null; this.context = null; this.frames = 0; this.lastUpdate = 0;
// devicePosition is shared-component compatibility evidence. Its actual
// effect in the Taobao host must be checked on a phone; no context method
// or successful callback is treated as proof that hardware changed.
return new Promise(function (resolve) {
that.setData({ running: false, busy: true, devicePosition: that.data.devicePosition === 'front' ? 'back' : 'front',
cameraSession: sequence, frameCount: 0, frameText: '尚未收到真实帧', poseText: '', fieldText: '', errorText: '',
snapshotUrl: '', snapshotText: '', status: '正在重新挂载相机;切换效果以实际画面为准…' }, function () {
that.mountCamera(sequence);
resolve();
});
});
},
onSnapshot: function () {
// Image decoding/draw callbacks may still use the shared canvas after a
// camera session changes. Release this lock only when that export settles.
if (this.exportInFlight) {
this.setData({ errorText: '上一帧照片仍在导出,请稍候再试。' });
return Promise.resolve();
}
if (!this.lastFrame || this.data.busy) {
if (!this.lastFrame) this.setData({ errorText: '尚未收到真实相机帧,无法验证照片导出。' });
return Promise.resolve();
}
var that = this, sequence = this.sequence, operation = {};
this.exportInFlight = operation;
this.setData({ busy: true, exporting: true, errorText: '', snapshotUrl: '', snapshotText: '' });
var work;
try { work = image.captureFrameToBase64(this.lastFrame, this, { defaultPixelFormat: config.arPixelFormat }); } catch (error) { work = Promise.reject(error); }
return Promise.resolve(work).then(function (value) {
if (!that.alive || !that.visible || sequence !== that.sequence) return;
that.setData({ snapshotUrl: value, snapshotText: 'JPEG ' + that.data.compressWidth + '×' + that.data.compressHeight + ',仅本页预览,未上传、未保存到相册。', status: '已导出真实帧照片,请核对方向和颜色。' });
}).catch(function (error) {
if (that.alive && that.visible && sequence === that.sequence) that.setData({ errorText: error.message || '照片导出失败,请记录帧字段与实际错误。' });
}).then(function () {
if (that.exportInFlight !== operation) return;
that.exportInFlight = null;
if (that.alive) {
var patch = { exporting: false };
if (sequence === that.sequence) patch.busy = false;
that.setData(patch);
}
});
},
onARStop: function (event) {
if (!this.acceptsCameraEvent(event)) return;
this.recordNativeEvent('onStop', event);
this.invalidateCalibration();
this.sequence++;
this.setData({ status: 'AR相机已停止。请重新开启后验证。', running: false, busy: false });
this.lastFrame = null; this.context = null; clearTimeout(this.frameTimer);
},
onError: function (event) {
if (!this.acceptsCameraEvent(event)) return;
var summary = this.recordNativeEvent('onError', event);
this.invalidateCalibration();
this.sequence++;
this.lastFrame = null; this.context = null; clearTimeout(this.frameTimer);
this.setData({ running: false, busy: false, status: 'AR 相机未通过真机验证。', errorText: '收到 AR 错误事件。' + (summary.metadata.join(';') || '未返回可安全展示的错误码,请复制诊断。') });
},
onStop: function () {
this.flushFrameDiagnostic();
this.invalidateCalibration();
this.sequence++; this.lastFrame = null; this.context = null; clearTimeout(this.frameTimer);
if (this.alive) this.setData({ running: false, busy: false, status: '已停止,帧缓存已清空。' });
},
onHide: function () {
this.visible = false; this.onStop();
if (this.alive) this.setData({ snapshotUrl: '', snapshotText: '', poseText: '', fieldText: '' });
},
onUnload: function () { this.onHide(); this.alive = false; }
});