var config = require('../config'), transport = require('./transport'); var token = '', user = null, storageKey = 'juzi.taobao.session.v1'; var sessionVersion = 0, authAttempt = 0, restoring = null, pendingAnalysis = null; var validationEpoch = 0, needsValidation = false; function assetUrl(path) { return config.assetBase.replace(/\/?$/, '/') + path.replace(/^assets\//, ''); } function isGuest() { return !!(user && user.kind === 'taobao_guest'); } function hasSession() { return !!(token && user); } function isSignedIn() { return hasSession() && !isGuest() && !needsValidation; } function clearTransient() { pendingAnalysis = null; var app = typeof getApp === 'function' && getApp(); if (app) { app.globalData.profile = null; app.globalData.photos = null; app.globalData.payload = null; } } function clear(keepSaved) { token = ''; user = null; sessionVersion++; authAttempt++; restoring = null; validationEpoch++; needsValidation = false; if (!keepSaved) { try { my.removeStorageSync({ key: storageKey }); } catch (_) {} } clearTransient(); } function resetForLaunch() { clear(true); } function suspendSession() { validationEpoch++; restoring = null; needsValidation = !!token && !isGuest(); } function stale() { var err = new Error('本次会话已结束,请重新进入'); err.canceled = true; return err; } function request(path, body) { var protectedRequest = !/^(me|challenge|session|guest-session|logout)$/.test(path); if (protectedRequest && needsValidation && token && !isGuest()) { return restore().then(function () { return request(path, body); }); } var requestToken = token, version = sessionVersion, epoch = validationEpoch; return transport.request('/api/taobao/' + path, body || {}, requestToken).then(function (data) { if (requestToken !== token || version !== sessionVersion) throw stale(); if (protectedRequest && !isGuest() && epoch !== validationEpoch) throw stale(); if (!data || !data.ok) { var err = new Error(data && data.message || '服务暂不可用,请稍后重试'); err.status = data && data.http_status; err.code = data && data.code; if (err.status === 401) clear(); throw err; } return data; }); } function authorize() { if (config.transport === 'direct') return Promise.resolve(); return new Promise(function (resolve, reject) { my.authorize({ scopes: 'scope.userInfo', success: resolve, fail: function () { reject(new Error('淘宝账号授权未完成,您也可以选择游客体验')); } }); }); } function startSession(consent, guest) { if (!consent) return Promise.reject(new Error('请先阅读并同意用户协议和隐私政策')); // A deliberate guest entry ends the previous identity before any async work. // Old /me success/401 responses then fail the session-version guard, and the // guest challenge/session never inherit the old Bearer or persisted account. if (guest) clear(); var ticket = ++authAttempt; return (guest ? Promise.resolve() : authorize()).then(function () { if (ticket !== authAttempt) throw stale(); return request('challenge'); }).then(function (data) { if (ticket !== authAttempt) throw stale(); return request(guest ? 'guest-session' : 'session', { consent: true, challenge: data.challenge }); }).then(function (data) { if (ticket !== authAttempt) throw stale(); if (!data.token || !data.user || !data.user.id || (guest && data.user.kind !== 'taobao_guest')) throw new Error('未能建立检测会话,请重试'); clearTransient(); sessionVersion++; validationEpoch++; needsValidation = false; token = data.token; user = data.user; if (guest) { try { my.removeStorageSync({ key: storageKey }); } catch (_) {} } else my.setStorageSync({ key: storageKey, data: { token: token, user: user } }); return user; }); } function login(consent) { return startSession(consent, false); } function loginGuest(consent) { return startSession(consent, true); } function restore() { // A product-detail round trip must preserve the current in-memory guest/report. if (isGuest()) return Promise.resolve(user); if (restoring) return restoring; if (!token) { try { var saved = my.getStorageSync({ key: storageKey }).data; if (saved && saved.user && saved.user.kind === 'taobao_guest') { my.removeStorageSync({ key: storageKey }); return Promise.resolve(null); } if (saved && saved.token) { token = saved.token; user = null; sessionVersion++; needsValidation = true; } } catch (_) {} } if (!token) return Promise.resolve(null); needsValidation = true; var version = sessionVersion, epoch = validationEpoch; restoring = request('me').then(function (data) { if (version !== sessionVersion || epoch !== validationEpoch) throw stale(); if (!data.user || !data.user.id || data.user.kind === 'taobao_guest') { clear(); return null; } user = data.user; needsValidation = false; return user; }); var result = restoring; return result.then(function (value) { if (restoring === result) restoring = null; return value; }, function (err) { if (restoring === result) restoring = null; throw err; }); } function canceled(signal) { if (signal && signal.canceled) { var e = new Error('已停止等待'); e.canceled = true; throw e; } } function wait(ms, signal) { return new Promise(function (resolve) { setTimeout(resolve, ms); }).then(function () { canceled(signal); }); } function poll(job, signal, deadline) { canceled(signal); if (Date.now() >= deadline) throw new Error(isGuest() ? '检测仍在处理中,请留在本次体验中重试查看;游客报告不保存到历史。' : '检测仍在处理中,请重试查看或稍后从历史报告查看'); return request('analysis', { job: job }).then(function (data) { canceled(signal); if (data.status !== 'complete') return wait(1000, signal).then(function () { return poll(job, signal, deadline); }); if (!data.payload || !data.payload.ok) throw new Error(data.payload && data.payload.message || '检测失败,请重试'); return data.payload; }); } function upload(view, photo, signal) { var b64 = photo.replace(/^data:image\/[^;]+;base64,/, ''), size = 64 * 1024; if (!b64 || b64.length > 4 * 1024 * 1024) return Promise.reject(new Error('照片过大,请重新拍摄')); var id = Date.now().toString(36) + '-' + Math.random().toString(36).slice(2), total = Math.ceil(b64.length / size); var chain = Promise.resolve(); for (var i = 0; i < total; i++) (function (index) { chain = chain.then(function () { canceled(signal); return request('photo', { view: view, upload_id: id, index: index, total: total, data: b64.slice(index * size, (index + 1) * size) }); }); })(i); return chain.then(function () { return id; }); } function createAnalysisAttempt() { return { requestId: 'tb-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2), ownerVersion: sessionVersion, job: '', status: 'ready', photoRefs: {}, photos: null, profile: null }; } function runAttempt(attempt, signal) { if (attempt.ownerVersion !== sessionVersion || !hasSession()) return Promise.reject(stale()); canceled(signal); if (attempt.running) return attempt.running.catch(function () {}).then(function () { return runAttempt(attempt, signal); }); var operation; if (attempt.job) operation = Promise.resolve(); else if (!attempt.profile) return Promise.reject(new Error('没有可恢复的检测任务,请重新拍摄')); else { attempt.status = 'uploading'; operation = ['front', 'left', 'right'].reduce(function (chain, view, i) { return chain.then(function () { if (attempt.photoRefs[view]) return; if (!attempt.photos || !attempt.photos[i]) throw new Error('检测照片不完整,请重新拍摄'); return upload(view, attempt.photos[i], signal).then(function (id) { attempt.photoRefs[view] = id; }); }); }, Promise.resolve()).then(function () { canceled(signal); attempt.status = 'submitting'; return request('skin-analyze', { photo_refs: attempt.photoRefs, profile: attempt.profile, request_id: attempt.requestId }); }).then(function (data) { attempt.job = data.job; attempt.photos = null; if (!attempt.job) throw new Error('未能取得检测任务,请重试'); }); } var running = operation.then(function () { canceled(signal); attempt.status = 'polling'; return poll(attempt.job, signal, Date.now() + 90000); }).then(function (payload) { attempt.status = 'complete'; if (pendingAnalysis === attempt) pendingAnalysis = null; return payload; }, function (err) { attempt.status = err.canceled ? 'paused' : 'failed'; if (err.status === 404 || err.status === 410) { err.message = '本次检测任务已失效,请重新拍摄;不会自动重复检测'; attempt.status = 'expired'; } throw err; }); attempt.running = running; return running.then(function (value) { if (attempt.running === running) attempt.running = null; return value; }, function (err) { if (attempt.running === running) attempt.running = null; throw err; }); } function analyze(photos, profile, signal, attempt) { if (config.mock) { var SS = require('../vendor/skin-score'), UI = require('../vendor/report-ui'); var vm = SS.buildViewModel(SS.MOCK_RESULT, SS.MOCK_SIDE, { realAge: profile.real_age, skinType: profile.daily_skin_type }); return Promise.resolve({ ok: true, result: SS.MOCK_RESULT, side: SS.MOCK_SIDE, vm: vm, presentation: UI.build(vm, SS.MOCK_RESULT) }); } attempt = attempt || createAnalysisAttempt(); if (!attempt.profile) { attempt.profile = Object.assign({}, profile); attempt.photos = photos.slice(); } pendingAnalysis = attempt; return Promise.resolve().then(function () { return runAttempt(attempt, signal); }); } function resumeAnalysis(attempt, signal) { attempt = attempt || pendingAnalysis; if (!attempt || attempt.status === 'expired') return Promise.reject(new Error('本次检测任务已失效,请重新拍摄')); return Promise.resolve().then(function () { return runAttempt(attempt, signal); }); } function wrinkleViews(jobToken, signal) { return request('wrinkle-views', { token: jobToken }).then(function (data) { return poll(data.job, signal, Date.now() + 90000); }); } function logout() { if (!token) { clear(); return Promise.resolve(); } return request('logout').catch(function (err) { if (err.status !== 401) throw err; }).then(function () { clear(); }); } function endGuest() { if (!isGuest()) return Promise.resolve(); var oldToken = token; clear(); try { Promise.resolve(transport.request('/api/taobao/logout', {}, oldToken)).catch(function () {}); } catch (_) {} return Promise.resolve(); } module.exports = { assetUrl: assetUrl, analyze: analyze, login: login, loginGuest: loginGuest, restore: restore, request: request, user: function () { return user; }, hasSession: hasSession, isGuest: isGuest, isSignedIn: isSignedIn, logout: logout, endGuest: endGuest, resetForLaunch: resetForLaunch, suspendSession: suspendSession, clear: clear, createAnalysisAttempt: createAnalysisAttempt, resumeAnalysis: resumeAnalysis, getPendingAnalysis: function () { return pendingAnalysis; }, discardAnalysis: function (attempt) { if (!attempt || pendingAnalysis === attempt) pendingAnalysis = null; }, wrinkleViews: wrinkleViews };