diff --git a/api/app/controller/api/Test.php b/api/app/controller/api/Test.php index 42f1538..0315f1c 100644 --- a/api/app/controller/api/Test.php +++ b/api/app/controller/api/Test.php @@ -581,14 +581,17 @@ class Test extends BaseController } /** - * 通过分享链接查看测试结果(无需登录,凭 st 校验防篡改) - * GET /api/test/share-detail?id=123&st=... + * 通过分享链接查看测试结果(无需登录) + * - 推荐:GET /api/test/share-detail?id=123&type=sbti(type 与库中 testType 一致即可) + * - 兼容旧链:GET ...&st=...(凭 st 校验,可不传 type) */ public function shareDetail() { $id = (int) Request::param('id', 0); $st = trim((string) Request::param('st', '')); - if ($id <= 0 || $st === '') { + $typeParam = trim((string) Request::param('type', '')); + + if ($id <= 0) { return error('缺少参数', 400); } @@ -597,16 +600,23 @@ class Test extends BaseController return error('记录不存在', 404); } - if (!$this->verifyShareToken($row, $st)) { - return error('分享链接无效或已失效', 403); - } - - $testType = (string) ($row['testType'] ?? ''); - if ($testType === 'resume') { + $rowTestType = (string) ($row['testType'] ?? ''); + if ($rowTestType === 'resume') { return error('该类型不支持分享查看', 403); } - $out = $this->buildTestDetailPayload($row); + if ($st !== '') { + if (!$this->verifyShareToken($row, $st)) { + return error('分享链接无效或已失效', 403); + } + } else { + if (!$this->shareDetailTypeMatchesRow($typeParam, $rowTestType)) { + return error('参数不匹配', 400); + } + } + + // 分享落地:访客不应因「答题人资料未完善」看到截断结果,也不应引导访客去完善自己的资料 + $out = $this->buildTestDetailPayload($row, true); return success(array_merge($out, [ 'shareToken' => $this->computeShareToken($row), @@ -614,9 +624,32 @@ class Test extends BaseController } /** - * 与 detail 接口一致的详情结构(data 为结果 JSON) + * 公开分享(无 st)时校验 URL 的 type 与记录 testType 一致,避免串页 */ - protected function buildTestDetailPayload(array $row): array + protected function shareDetailTypeMatchesRow(string $typeParam, string $rowTestType): bool + { + $a = strtolower(trim($typeParam)); + $b = strtolower(trim($rowTestType)); + if ($a === '' || $b === '') { + return false; + } + if ($a === $b) { + return true; + } + // 人脸:库内可能是 face 或 ai + if (($a === 'ai' || $a === 'face') && ($b === 'ai' || $b === 'face')) { + return true; + } + + return false; + } + + /** + * 与 detail 接口一致的详情结构(data 为结果 JSON) + * + * @param bool $forShareViewer true=分享链接打开(/api/test/share-detail):不因答题人资料未完善而截断结果;未付费仍按定价截断 + */ + protected function buildTestDetailPayload(array $row, bool $forShareViewer = false): array { $raw = $row['resultData'] ?? ($row['result'] ?? null); $data = null; @@ -631,15 +664,16 @@ class Test extends BaseController $needPaymentToUnlock = $requiresPayment && !$isPaid && $paidAmount > 0; $subjectUserId = (int) ($row['userId'] ?? 0); $profileIncomplete = $subjectUserId > 0 ? !self::isWechatProfileComplete($subjectUserId) : false; + $applyProfileGate = $profileIncomplete && !$forShareViewer; if ($data !== null && is_array($data)) { if (in_array($testType, ['face', 'ai'], true)) { $eid = isset($row['enterpriseId']) ? (int) $row['enterpriseId'] : 0; $data = self::stripFaceAiSbtiIfEnterpriseDisabled($data, $eid > 0 ? $eid : null); - if ($needPaymentToUnlock || $profileIncomplete) { + if ($needPaymentToUnlock || $applyProfileGate) { $data = self::filterFaceResultToPreview($data); } - } elseif ($needPaymentToUnlock || $profileIncomplete) { + } elseif ($needPaymentToUnlock || $applyProfileGate) { $data = $this->filterResultToPartial($testType, $data); } } @@ -654,7 +688,7 @@ class Test extends BaseController 'paidAmount' => $paidAmount, 'amountYuan' => $paidAmount > 0 ? round($paidAmount / 100, 2) : 0, 'needPaymentToUnlock' => $needPaymentToUnlock, - 'profileIncomplete' => $profileIncomplete, + 'profileIncomplete' => $forShareViewer ? false : $profileIncomplete, 'orderId' => isset($row['orderId']) ? (int) $row['orderId'] : null, 'paidAt' => isset($row['paidAt']) ? (int) $row['paidAt'] : null, ]; @@ -813,7 +847,11 @@ class Test extends BaseController return error('保存测试结果失败', 500); } - return success(null, '提交成功'); + // 返回记录 id,供小程序结果页带参跳转与分享(shareToken 由 /api/test/detail 下发) + return success([ + 'id' => (int) $id, + 'testType' => $testType, + ], '提交成功'); } /** diff --git a/douyin-miniprogram/app.js b/douyin-miniprogram/app.js index ab88361..4d6a9bf 100644 --- a/douyin-miniprogram/app.js +++ b/douyin-miniprogram/app.js @@ -368,13 +368,18 @@ App({ tt.setStorageSync(key, result) this.globalData[key] = result - if (this.globalData.token) { - const scope = this.globalData.appScope || 'personal' - const storedUser = tt.getStorageSync('userInfo') || null - const enterpriseId = - scope === 'enterprise' - ? (this.globalData.enterpriseIdFromScene || (this.globalData.userInfo && this.globalData.userInfo.enterpriseId) || (storedUser && storedUser.enterpriseId) || null) - : null + if (!this.globalData.token) { + return Promise.resolve({}) + } + + const scope = this.globalData.appScope || 'personal' + const storedUser = tt.getStorageSync('userInfo') || null + const enterpriseId = + scope === 'enterprise' + ? (this.globalData.enterpriseIdFromScene || (this.globalData.userInfo && this.globalData.userInfo.enterpriseId) || (storedUser && storedUser.enterpriseId) || null) + : null + + return new Promise((resolve) => { tt.request({ url: `${this.globalData.apiBase}/api/test/submit`, method: 'POST', @@ -390,9 +395,17 @@ App({ enterpriseId: enterpriseId || undefined, testDuration: result.testDuration || 0, timestamp: new Date().toISOString() - } + }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data && typeof res.data.data === 'object') { + resolve(res.data.data) + } else { + resolve({}) + } + }, + fail: () => resolve({}) }) - } + }) }, getTestResult(type) { diff --git a/douyin-miniprogram/pages/index/result.js b/douyin-miniprogram/pages/index/result.js index ca4f444..1ed3f15 100644 --- a/douyin-miniprogram/pages/index/result.js +++ b/douyin-miniprogram/pages/index/result.js @@ -106,8 +106,6 @@ Page({ onLoad(options) { this._payInfoSetByDetail = false - const id = options && options.id - const type = options && options.type // 同步审核模式 this.setData({ reviewMode: !!app.globalData.reviewMode }) @@ -136,55 +134,95 @@ Page({ }).catch(() => {}) } - // 从历史记录进入:根据ID从后端读取数据库中的结果 - if (id && type === 'ai') { - const token = app.globalData.token || tt.getStorageSync('token') - const apiBase = app.globalData.apiBase - if (!token || !apiBase) { - tt.showToast({ title: '未登录,无法读取历史记录', icon: 'none' }) + const idStr = options && options.id != null && options.id !== '' ? String(options.id) : '' + const st = options && options.st ? String(options.st).trim() : '' + const rawType = options && options.type ? String(options.type).toLowerCase() : '' + const faceType = rawType || (idStr ? 'ai' : '') + + if (idStr && (faceType === 'ai' || faceType === 'face')) { + this.setData({ testResultId: idStr }) + this.loadFaceRecordById(idStr, st, faceType) + return + } + + if (idStr) { + tt.showToast({ title: '链接参数无效', icon: 'none' }) + setTimeout(() => tt.navigateBack(), 1500) + return + } + + this.startAnalysis() + }, + + loadFaceRecordById(id, st, typeParam) { + const apiBase = app.globalData?.apiBase || '' + if (!apiBase) { + tt.showToast({ title: '配置异常', icon: 'none' }) + return + } + const token = app.globalData.token || tt.getStorageSync('token') || '' + + const applyPayload = (payload) => { + tt.hideLoading() + const apiData = payload.data || payload + this.initPayInfoFromRuntime(!!payload.requiresPayment, !!payload.isPaid, payload) + this.processResult(apiData) + } + + const loadDetailAuthed = () => { + if (!token) { + tt.hideLoading() + tt.showToast({ title: '未登录,无法查看该记录', icon: 'none' }) setTimeout(() => tt.navigateBack(), 1500) return } - - tt.showLoading({ title: '加载历史记录...' }) + tt.showLoading({ title: '加载中...' }) tt.request({ url: `${apiBase}/api/test/detail`, method: 'GET', - header: { - 'Authorization': `Bearer ${token}` - }, + header: { Authorization: `Bearer ${token}` }, data: { id }, success: (res) => { - tt.hideLoading() - if (res.statusCode === 200 && res.data && res.data.data) { - const payload = res.data.data - const apiData = payload.data || payload - // 历史详情场景下,记录当前测试记录ID - this.setData({ testResultId: id }) - // 先确定付费状态(设置 _payInfoSetByDetail 标记),再渲染结果 - // 避免 processResult 内部异步拉全局配置覆盖掉数据库级别的付费判定 - this.initPayInfoFromRuntime( - !!payload.requiresPayment, - !!payload.isPaid, - payload - ) - this.processResult(apiData) + if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) { + applyPayload(res.data.data) } else { + tt.hideLoading() tt.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) setTimeout(() => tt.navigateBack(), 1500) } }, fail: () => { tt.hideLoading() - tt.showToast({ title: '网络错误,加载失败', icon: 'none' }) + tt.showToast({ title: '网络错误', icon: 'none' }) setTimeout(() => tt.navigateBack(), 1500) } }) - return } - // 正常从拍照流程进入:调用 /api/analyze - this.startAnalysis() + tt.showLoading({ title: '加载中...' }) + const data = { + id: String(id), + type: typeParam === 'face' ? 'face' : 'ai' + } + if (st) data.st = st + + tt.request({ + url: `${apiBase}/api/test/share-detail`, + method: 'GET', + data, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) { + applyPayload(res.data.data) + return + } + tt.hideLoading() + loadDetailAuthed() + }, + fail: () => { + tt.hideLoading() + loadDetailAuthed() + } + }) }, onShow() { @@ -606,7 +644,14 @@ Page({ const r = this.data.result const rm = this.data.reviewMode const t = rm ? '测试结果' : (this.data.aiAnalysisText || '分析') - const { getSharePathByScope } = require('../../utils/share') + const { getResultSharePath, getSharePathByScope } = require('../../utils/share') + const tid = this.data.testResultId + if (tid) { + return { + title: `${t}:我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, + path: getResultSharePath('/pages/index/result', { id: tid, type: 'ai' }) + } + } return { title: `${t}:我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, path: getSharePathByScope('/pages/index/index') @@ -617,10 +662,11 @@ Page({ const r = this.data.result const rm = this.data.reviewMode const t = rm ? '测试结果' : (this.data.aiAnalysisText || '分析') - const { buildShareQuery } = require('../../utils/share') + const { getResultShareTimelineQuery, buildShareQuery } = require('../../utils/share') + const tid = this.data.testResultId return { title: `${t}:我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, - query: buildShareQuery() + query: tid ? getResultShareTimelineQuery({ id: tid, type: 'ai' }) : buildShareQuery() } } }) diff --git a/douyin-miniprogram/pages/promo/index.js b/douyin-miniprogram/pages/promo/index.js index 1835f6b..b01516c 100644 --- a/douyin-miniprogram/pages/promo/index.js +++ b/douyin-miniprogram/pages/promo/index.js @@ -34,12 +34,23 @@ Page({ }, onLoad() { - this.loadStats() - this.loadBindings(true) + app.ensureLogin() + .then((ok) => { + if (!ok) { + tt.showToast({ title: '请先登录后查看推广中心', icon: 'none' }) + return + } + this.loadStats() + this.loadBindings(true) + }) + .catch(() => { + tt.showToast({ title: '登录失败,请重试', icon: 'none' }) + }) }, onShow() { - this.loadStats() + const token = app.globalData.token || tt.getStorageSync('token') + if (token) this.loadStats() }, /** 加载推广统计数据 */ diff --git a/douyin-miniprogram/pages/promo/poster.js b/douyin-miniprogram/pages/promo/poster.js index 4562585..cef9b70 100644 --- a/douyin-miniprogram/pages/promo/poster.js +++ b/douyin-miniprogram/pages/promo/poster.js @@ -1,3 +1,4 @@ +const app = getApp() const { getApiBase } = require('../../utils/request') Page({ @@ -8,7 +9,18 @@ Page({ }, onLoad() { - this.loadPoster() + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ loading: false, loadingText: '请先登录后生成海报' }) + tt.showToast({ title: '请先登录', icon: 'none' }) + return + } + this.loadPoster() + }) + .catch(() => { + this.setData({ loading: false, loadingText: '登录失败,请重试' }) + }) }, /** 从后端接口下载完整合成海报 */ diff --git a/douyin-miniprogram/pages/result/disc.js b/douyin-miniprogram/pages/result/disc.js index 3ca1b74..a831ec5 100644 --- a/douyin-miniprogram/pages/result/disc.js +++ b/douyin-miniprogram/pages/result/disc.js @@ -44,8 +44,8 @@ Page({ }, onLoad(options) { - const id = options && options.id - const type = options && options.type + const id = options && options.id != null && options.id !== '' ? String(options.id) : '' + const type = options && options.type ? String(options.type).toLowerCase() : '' if (id && type === 'disc') { this.setData({ testResultId: id }) this.loadDetail(id) diff --git a/douyin-miniprogram/pages/result/mbti.js b/douyin-miniprogram/pages/result/mbti.js index 1167b24..f67168a 100644 --- a/douyin-miniprogram/pages/result/mbti.js +++ b/douyin-miniprogram/pages/result/mbti.js @@ -31,8 +31,8 @@ Page({ }, onLoad(options) { - const id = options && options.id - const type = options && options.type + const id = options && options.id != null && options.id !== '' ? String(options.id) : '' + const type = options && options.type ? String(options.type).toLowerCase() : '' if (id && type === 'mbti') { this.setData({ testResultId: id }) diff --git a/douyin-miniprogram/pages/result/pdp.js b/douyin-miniprogram/pages/result/pdp.js index 90bc987..0fdd763 100644 --- a/douyin-miniprogram/pages/result/pdp.js +++ b/douyin-miniprogram/pages/result/pdp.js @@ -48,8 +48,8 @@ Page({ }, onLoad(options) { - const id = options && options.id - const type = options && options.type + const id = options && options.id != null && options.id !== '' ? String(options.id) : '' + const type = options && options.type ? String(options.type).toLowerCase() : '' if (id && type === 'pdp') { this.setData({ testResultId: id }) this.loadDetail(id) diff --git a/douyin-miniprogram/pages/result/resume.js b/douyin-miniprogram/pages/result/resume.js index 879bddb..0f10c85 100644 --- a/douyin-miniprogram/pages/result/resume.js +++ b/douyin-miniprogram/pages/result/resume.js @@ -28,12 +28,28 @@ Page({ const fileUrl = options.fileUrl ? decodeURIComponent(options.fileUrl) : '' this.setData({ fileUrl }) - // 从历史记录进入:直接读已存的数据,不重新生成 - if (options.id && options.type === 'resume') { - this.loadFromHistory(options.id) - } else { - this.fetchResumeAnalysis() + const run = () => { + if (options.id && options.type === 'resume') { + this.loadFromHistory(options.id) + } else { + this.fetchResumeAnalysis() + } } + + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ + loading: false, + error: '请先登录后查看报告;若已登录请下拉重试或返回重进。' + }) + return + } + run() + }) + .catch(() => { + this.setData({ loading: false, error: '登录失败,请返回重试。' }) + }) }, onShow() { diff --git a/douyin-miniprogram/pages/test-select/index.js b/douyin-miniprogram/pages/test-select/index.js index 4ca9b4b..1a29e2f 100644 --- a/douyin-miniprogram/pages/test-select/index.js +++ b/douyin-miniprogram/pages/test-select/index.js @@ -6,10 +6,12 @@ Page({ permFace: true, permMbti: true, permPdp: true, - permDisc: true + permDisc: true, + allTestsDisabled: false }, onLoad() { + app.ensureLogin().catch(() => {}) this._syncPerms() }, @@ -19,11 +21,16 @@ Page({ _syncPerms() { const p = app.globalData.enterprisePermissions + const permFace = !p || p.face !== false + const permMbti = !p || p.mbti !== false + const permPdp = !p || p.pdp !== false + const permDisc = !p || p.disc !== false this.setData({ - permFace: !p || p.face !== false, - permMbti: !p || p.mbti !== false, - permPdp: !p || p.pdp !== false, - permDisc: !p || p.disc !== false + permFace, + permMbti, + permPdp, + permDisc, + allTestsDisabled: p && !permMbti && !permPdp && !permDisc }) }, diff --git a/douyin-miniprogram/pages/test-select/index.ttml b/douyin-miniprogram/pages/test-select/index.ttml index de65326..e5a013d 100644 --- a/douyin-miniprogram/pages/test-select/index.ttml +++ b/douyin-miniprogram/pages/test-select/index.ttml @@ -37,4 +37,8 @@ + + + 当前环境下暂未开放问卷测试,请联系管理员或从首页进入。 + diff --git a/douyin-miniprogram/pages/test-select/index.ttss b/douyin-miniprogram/pages/test-select/index.ttss index 8772159..1b7a7cc 100644 --- a/douyin-miniprogram/pages/test-select/index.ttss +++ b/douyin-miniprogram/pages/test-select/index.ttss @@ -35,6 +35,17 @@ line-height: 1.6; } +.perm-disabled-hint { + text-align: center; + padding: 32rpx; +} + +.perm-disabled-text { + font-size: 28rpx; + color: #888; + line-height: 1.6; +} + /* 入口卡片(与 result 页 next-btn 同色系) */ .entry-card { display: flex; diff --git a/douyin-miniprogram/pages/test/disc.js b/douyin-miniprogram/pages/test/disc.js index 9b1e747..95b88d2 100644 --- a/douyin-miniprogram/pages/test/disc.js +++ b/douyin-miniprogram/pages/test/disc.js @@ -25,8 +25,17 @@ Page({ timer: null, onLoad() { - loadQuestions('disc', {}) + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ loading: false }) + tt.showToast({ title: '登录失败,请重试', icon: 'none' }) + return Promise.reject(new Error('login')) + } + return loadQuestions('disc', {}) + }) .then((questions) => { + if (!questions) return if (!questions.length) { tt.showToast({ title: '暂无题目', icon: 'none' }) this.setData({ loading: false }) @@ -47,6 +56,7 @@ Page({ this.startTimer() }) .catch((err) => { + if (err && err.message === 'login') return this.setData({ loading: false }) tt.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }) }) @@ -179,9 +189,16 @@ Page({ tt.setStorageSync('discResult', resultData) try { require('../../utils/analytics').track('test_complete', { type: 'disc', result: dominantType, duration: resultData.testDuration }) } catch (e) {} if (app && typeof app.saveTestResult === 'function') { - app.saveTestResult('disc', resultData) + app.saveTestResult('disc', resultData).then((extra) => { + const rid = extra && extra.id + if (rid) { + tt.redirectTo({ url: `/pages/result/disc?id=${rid}&type=disc` }) + } else { + tt.redirectTo({ url: '/pages/result/disc' }) + } + }) + } else { + tt.redirectTo({ url: '/pages/result/disc' }) } - - tt.redirectTo({ url: '/pages/result/disc' }) } }) diff --git a/douyin-miniprogram/pages/test/mbti.js b/douyin-miniprogram/pages/test/mbti.js index 935a13b..6bbefe0 100644 --- a/douyin-miniprogram/pages/test/mbti.js +++ b/douyin-miniprogram/pages/test/mbti.js @@ -27,8 +27,17 @@ Page({ timer: null, onLoad() { - loadQuestions('mbti', {}) + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ loading: false }) + tt.showToast({ title: '登录失败,请重试', icon: 'none' }) + return Promise.reject(new Error('login')) + } + return loadQuestions('mbti', {}) + }) .then((questions) => { + if (!questions) return const total = questions.length if (!total) { tt.showToast({ title: '暂无题目', icon: 'none' }) @@ -52,6 +61,7 @@ Page({ this.startTimer() }) .catch((err) => { + if (err && err.message === 'login') return this.setData({ loading: false }) tt.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }) }) @@ -213,10 +223,14 @@ Page({ } tt.setStorageSync('mbtiResult', resultData) try { require('../../utils/analytics').track('test_complete', { type: 'mbti', result: result.mbtiType, confidence: result.confidence, duration: resultData.testDuration }) } catch (e) {} - app.saveTestResult('mbti', resultData) - tt.redirectTo({ - url: '/pages/result/mbti' + app.saveTestResult('mbti', resultData).then((extra) => { + const rid = extra && extra.id + if (rid) { + tt.redirectTo({ url: `/pages/result/mbti?id=${rid}&type=mbti` }) + } else { + tt.redirectTo({ url: '/pages/result/mbti' }) + } }) }, diff --git a/douyin-miniprogram/pages/test/pdp.js b/douyin-miniprogram/pages/test/pdp.js index ee73df0..f7063a4 100644 --- a/douyin-miniprogram/pages/test/pdp.js +++ b/douyin-miniprogram/pages/test/pdp.js @@ -25,8 +25,17 @@ Page({ timer: null, onLoad() { - loadQuestions('pdp', {}) + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ loading: false }) + tt.showToast({ title: '登录失败,请重试', icon: 'none' }) + return Promise.reject(new Error('login')) + } + return loadQuestions('pdp', {}) + }) .then((questions) => { + if (!questions) return if (!questions.length) { tt.showToast({ title: '暂无题目', icon: 'none' }) this.setData({ loading: false }) @@ -47,6 +56,7 @@ Page({ this.startTimer() }) .catch((err) => { + if (err && err.message === 'login') return this.setData({ loading: false }) tt.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }) }) @@ -182,9 +192,16 @@ Page({ tt.setStorageSync('pdpResult', resultData) try { require('../../utils/analytics').track('test_complete', { type: 'pdp', result: dominantType, duration: resultData.testDuration }) } catch (e) {} if (app && typeof app.saveTestResult === 'function') { - app.saveTestResult('pdp', resultData) + app.saveTestResult('pdp', resultData).then((extra) => { + const rid = extra && extra.id + if (rid) { + tt.redirectTo({ url: `/pages/result/pdp?id=${rid}&type=pdp` }) + } else { + tt.redirectTo({ url: '/pages/result/pdp' }) + } + }) + } else { + tt.redirectTo({ url: '/pages/result/pdp' }) } - - tt.redirectTo({ url: '/pages/result/pdp' }) } }) diff --git a/douyin-miniprogram/utils/share.js b/douyin-miniprogram/utils/share.js index 6b421b2..d9c0928 100644 --- a/douyin-miniprogram/utils/share.js +++ b/douyin-miniprogram/utils/share.js @@ -40,4 +40,44 @@ function getSharePathByScope(personalBasePath) { return getSharePath(base) } -module.exports = { buildShareQuery, getSharePath, getSharePathByScope } +/** 测试结果页分享:id + type + fs=1 + 邀请参数 */ +function getResultSharePath(resultPagePath, opts) { + const id = opts && opts.id + const type = opts && opts.type + if (!id || !type) { + return getSharePathByScope('/pages/index/index') + } + const q = + 'id=' + + encodeURIComponent(String(id)) + + '&type=' + + encodeURIComponent(String(type)) + + '&fs=1' + const inv = buildShareQuery() + return inv ? resultPagePath + '?' + q + '&' + inv : resultPagePath + '?' + q +} + +/** 单页模式 / 部分场景分享用 query:id + type + fs=1 + 邀请参数 */ +function getResultShareTimelineQuery(opts) { + const id = opts && opts.id + const type = opts && opts.type + if (!id || !type) { + return buildShareQuery() + } + const base = + 'id=' + + encodeURIComponent(String(id)) + + '&type=' + + encodeURIComponent(String(type)) + + '&fs=1' + const inv = buildShareQuery() + return inv ? base + '&' + inv : base +} + +module.exports = { + buildShareQuery, + getSharePath, + getSharePathByScope, + getResultSharePath, + getResultShareTimelineQuery +} diff --git a/miniprogram/app.js b/miniprogram/app.js index 6273602..ff6ad56 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -47,8 +47,8 @@ App({ // 超管配置的默认企业 ID(无 scene/eid 等入口参数时回落) defaultEnterpriseId: null, // API基础地址(开发时用本地,生产环境替换为实际域名) - apiBase: 'https://mbtiapi.quwanzhi.com', - //apiBase: 'http://mbti.com', + //apiBase: 'https://mbtiapi.quwanzhi.com', + apiBase: 'http://mbti.com', // VIP信息 vipInfo: null, // 测试次数 @@ -418,16 +418,19 @@ App({ }) }, - // 保存测试结果 + // 保存测试结果(同步服务端后 resolve { id, testType },用于结果页 URL 与分享) saveTestResult(type, result) { const { getEnterpriseIdForApiPayload } = require('./utils/enterpriseContext.js') const key = `${type}Result` wx.setStorageSync(key, result) this.globalData[key] = result - - // 同步到服务器(需携带 token,后端从 JWT 解析 userId) - if (this.globalData.token) { - const enterpriseId = getEnterpriseIdForApiPayload() + + if (!this.globalData.token) { + return Promise.resolve({}) + } + + const enterpriseId = getEnterpriseIdForApiPayload() + return new Promise((resolve) => { wx.request({ url: `${this.globalData.apiBase}/api/test/submit`, method: 'POST', @@ -443,9 +446,17 @@ App({ enterpriseId: enterpriseId != null ? enterpriseId : undefined, testDuration: result.testDuration || 0, timestamp: new Date().toISOString() - } + }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data && typeof res.data.data === 'object') { + resolve(res.data.data) + } else { + resolve({}) + } + }, + fail: () => resolve({}) }) - } + }) }, // 获取测试结果 diff --git a/miniprogram/pages/index/result.js b/miniprogram/pages/index/result.js index 21c02f4..65bad75 100644 --- a/miniprogram/pages/index/result.js +++ b/miniprogram/pages/index/result.js @@ -108,8 +108,6 @@ Page({ onLoad(options) { this._payInfoSetByDetail = false - const id = options && options.id - const type = options && options.type // 加载文案配置(分析中提示、报告标题等) const tc = app.globalData.textConfig @@ -135,55 +133,101 @@ Page({ }).catch(() => {}) } - // 从历史记录进入:根据ID从后端读取数据库中的结果 - if (id && type === 'ai') { - const token = app.globalData.token || wx.getStorageSync('token') - const apiBase = app.globalData.apiBase - if (!token || !apiBase) { - wx.showToast({ title: '未登录,无法读取历史记录', icon: 'none' }) + const idStr = options && options.id != null && options.id !== '' ? String(options.id) : '' + const st = options && options.st ? String(options.st).trim() : '' + const rawType = options && options.type ? String(options.type).toLowerCase() : '' + // 人脸记录在库中可能是 face 或 ai;仅 id 时默认识别为人脸报告(兼容旧分享) + const faceType = rawType || (idStr ? 'ai' : '') + + // 带记录 id:只读库展示,禁止落入 startAnalysis 造成「二次分析」 + if (idStr && (faceType === 'ai' || faceType === 'face')) { + this.setData({ testResultId: idStr }) + this.loadFaceRecordById(idStr, st, faceType) + return + } + + if (idStr) { + wx.showToast({ title: '链接参数无效', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + return + } + + // 无 id:正常从拍照流程进入,调用 /api/analyze + this.startAnalysis() + }, + + /** + * 加载人脸报告:优先免登录 share-detail(好友分享),失败且已登录再 detail + */ + loadFaceRecordById(id, st, typeParam) { + const apiBase = app.globalData?.apiBase || '' + if (!apiBase) { + wx.showToast({ title: '配置异常', icon: 'none' }) + return + } + const token = app.globalData.token || wx.getStorageSync('token') || '' + + const applyPayload = (payload) => { + wx.hideLoading() + const apiData = payload.data || payload + this.initPayInfoFromRuntime(!!payload.requiresPayment, !!payload.isPaid, payload) + this.processResult(apiData) + } + + const loadDetailAuthed = () => { + if (!token) { + wx.hideLoading() + wx.showToast({ title: '未登录,无法查看该记录', icon: 'none' }) setTimeout(() => wx.navigateBack(), 1500) return } - - wx.showLoading({ title: '加载历史记录...' }) + wx.showLoading({ title: '加载中...' }) wx.request({ url: `${apiBase}/api/test/detail`, method: 'GET', - header: { - 'Authorization': `Bearer ${token}` - }, + header: { Authorization: `Bearer ${token}` }, data: { id }, success: (res) => { - wx.hideLoading() - if (res.statusCode === 200 && res.data && res.data.data) { - const payload = res.data.data - const apiData = payload.data || payload - // 历史详情场景下,记录当前测试记录ID - this.setData({ testResultId: id }) - // 先确定付费状态(设置 _payInfoSetByDetail 标记),再渲染结果 - // 避免 processResult 内部异步拉全局配置覆盖掉数据库级别的付费判定 - this.initPayInfoFromRuntime( - !!payload.requiresPayment, - !!payload.isPaid, - payload - ) - this.processResult(apiData) + if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) { + applyPayload(res.data.data) } else { + wx.hideLoading() wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) setTimeout(() => wx.navigateBack(), 1500) } }, fail: () => { wx.hideLoading() - wx.showToast({ title: '网络错误,加载失败', icon: 'none' }) + wx.showToast({ title: '网络错误', icon: 'none' }) setTimeout(() => wx.navigateBack(), 1500) } }) - return } - // 正常从拍照流程进入:调用 /api/analyze - this.startAnalysis() + wx.showLoading({ title: '加载中...' }) + const data = { + id: String(id), + type: typeParam === 'face' ? 'face' : 'ai' + } + if (st) data.st = st + + wx.request({ + url: `${apiBase}/api/test/share-detail`, + method: 'GET', + data, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) { + applyPayload(res.data.data) + return + } + wx.hideLoading() + loadDetailAuthed() + }, + fail: () => { + wx.hideLoading() + loadDetailAuthed() + } + }) }, onShow() { @@ -631,7 +675,14 @@ Page({ onShareAppMessage() { const r = this.data.result const t = this.data.aiAnalysisText || '智能分析' - const { getSharePathByScope } = require('../../utils/share') + const { getResultSharePath, getSharePathByScope } = require('../../utils/share') + const tid = this.data.testResultId + if (tid) { + return { + title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, + path: getResultSharePath('/pages/index/result', { id: tid, type: 'ai' }) + } + } return { title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, path: getSharePathByScope('/pages/index/index') @@ -641,7 +692,14 @@ Page({ onShareTimeline() { const r = this.data.result const t = this.data.aiAnalysisText || '智能分析' - const { buildShareQuery } = require('../../utils/share') + const { getResultShareTimelineQuery, buildShareQuery } = require('../../utils/share') + const tid = this.data.testResultId + if (tid) { + return { + title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, + query: getResultShareTimelineQuery({ id: tid, type: 'ai' }) + } + } return { title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, query: buildShareQuery() diff --git a/miniprogram/pages/promo/index.js b/miniprogram/pages/promo/index.js index 65bb867..7df81cb 100644 --- a/miniprogram/pages/promo/index.js +++ b/miniprogram/pages/promo/index.js @@ -34,12 +34,24 @@ Page({ }, onLoad() { - this.loadStats() - this.loadBindings(true) + // 分享直达时 silentLogin 可能未完成,先 ensureLogin 再拉推广数据,避免 401 空白 + app.ensureLogin() + .then((ok) => { + if (!ok) { + wx.showToast({ title: '请先登录后查看推广中心', icon: 'none' }) + return + } + this.loadStats() + this.loadBindings(true) + }) + .catch(() => { + wx.showToast({ title: '登录失败,请重试', icon: 'none' }) + }) }, onShow() { - this.loadStats() + const token = app.globalData.token || wx.getStorageSync('token') + if (token) this.loadStats() }, /** 加载推广统计数据 */ diff --git a/miniprogram/pages/promo/poster.js b/miniprogram/pages/promo/poster.js index f1ed8fa..27e8879 100644 --- a/miniprogram/pages/promo/poster.js +++ b/miniprogram/pages/promo/poster.js @@ -1,3 +1,4 @@ +const app = getApp() const { getApiBase } = require('../../utils/request') const { getEffectiveEnterpriseId } = require('../../utils/enterpriseContext.js') @@ -9,7 +10,18 @@ Page({ }, onLoad() { - this.loadPoster() + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ loading: false, loadingText: '请先登录后生成海报' }) + wx.showToast({ title: '请先登录', icon: 'none' }) + return + } + this.loadPoster() + }) + .catch(() => { + this.setData({ loading: false, loadingText: '登录失败,请重试' }) + }) }, /** 从后端接口下载完整合成海报 */ diff --git a/miniprogram/pages/result/disc.js b/miniprogram/pages/result/disc.js index 1cf211c..8d03ce1 100644 --- a/miniprogram/pages/result/disc.js +++ b/miniprogram/pages/result/disc.js @@ -43,18 +43,24 @@ Page({ testResultId: null, shareToken: '', hasReloadedAfterPay: false, + fromShare: false }, onLoad(options) { - const id = options && options.id - const type = options && options.type + const fromShareFs = + options && (String(options.fs) === '1' || options.from === 'share') + const id = options && options.id != null && options.id !== '' ? String(options.id) : '' + const st = options && options.st ? String(options.st).trim() : '' + const type = options && options.type ? String(options.type).toLowerCase() : '' + if (id && type === 'disc') { - this.setData({ testResultId: id }) - if (options.st) { - this.loadShareDetail(id, options.st) - } else { - this.loadDetail(id) - } + this.setData({ testResultId: id, fromShare: !!fromShareFs }) + this.loadShareDetail(id, st, 'disc') + return + } + if (id && st) { + this.setData({ testResultId: id, fromShare: true }) + this.loadShareDetail(id, st, '') return } const raw = wx.getStorageSync('discResult') @@ -86,7 +92,11 @@ Page({ isPaid, amountYuan: needPaymentToUnlock ? amountYuan : 0 } - this.setData({ payInfo }) + const patch = { payInfo } + if (payload.id != null && payload.id !== '') { + patch.testResultId = String(payload.id) + } + this.setData(patch) }, loadDetail(id) { @@ -111,14 +121,17 @@ Page({ }) }, - loadShareDetail(id, st) { + loadShareDetail(id, st, testType) { const apiBase = app.globalData?.apiBase || '' if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }); return } + const data = { id: String(id) } + if (st) data.st = st + if (testType) data.type = testType wx.showLoading({ title: '加载中...' }) wx.request({ url: `${apiBase}/api/test/share-detail`, method: 'GET', - data: { id, st }, + data, success: (res) => { if (res.statusCode === 200 && res.data && res.data.code === 200) { this.applyDetailPayload(res.data.data || {}) @@ -185,6 +198,10 @@ Page({ wx.navigateTo({ url: '/pages/test/disc' }) }, + goWantTest() { + wx.navigateTo({ url: '/pages/test/disc' }) + }, + goHome() { const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' if (scope === 'enterprise') { @@ -201,8 +218,7 @@ Page({ title: `我的DISC类型是${result?.dominantType}型(${result?.description?.title}),来测测你的吧!`, path: getResultSharePath('/pages/result/disc', { id: this.data.testResultId, - type: 'disc', - shareToken: this.data.shareToken + type: 'disc' }) } }, @@ -214,8 +230,7 @@ Page({ title: `我的DISC类型是${result?.dominantType}型(${result?.description?.title}),来测测你的吧!`, query: getResultShareTimelineQuery({ id: this.data.testResultId, - type: 'disc', - shareToken: this.data.shareToken + type: 'disc' }) } } diff --git a/miniprogram/pages/result/disc.wxml b/miniprogram/pages/result/disc.wxml index 4e89e27..69c7923 100644 --- a/miniprogram/pages/result/disc.wxml +++ b/miniprogram/pages/result/disc.wxml @@ -1,7 +1,7 @@ - - + + 您的DISC性格类型 @@ -25,7 +25,7 @@ - + 完整报告需完善资料 请补全头像、昵称并绑定手机号后查看完整 DISC 报告。 @@ -88,4 +88,7 @@ --> + + + diff --git a/miniprogram/pages/result/disc.wxss b/miniprogram/pages/result/disc.wxss index 61eae0f..0c1b64d 100644 --- a/miniprogram/pages/result/disc.wxss +++ b/miniprogram/pages/result/disc.wxss @@ -15,6 +15,14 @@ padding-bottom: 64rpx; } +.content-container--with-fixed-footer { + padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom)); +} + +.content-scroll--with-fixed-footer { + height: 100vh; +} + .type-card, .analysis-card { background-color: #fff; @@ -195,3 +203,35 @@ .paywall-btn { position: absolute; left: 5%; right: 5%; width: 90%; bottom: 132rpx; z-index: 3; padding: 20rpx 0; border-radius: 999rpx; background: linear-gradient(135deg, #3B82F6 0%, #60a5fa 100%); box-shadow: 0 8rpx 24rpx rgba(59,130,246,0.35); display: flex; flex-direction: row; align-items: baseline; justify-content: center; gap: 12rpx; } .paywall-btn-main { font-size: 30rpx; color: #fff; font-weight: 600; } .paywall-btn-price { font-size: 24rpx; color: #ffe5f0; } + +.result-share-footer { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 200; + box-sizing: border-box; + width: 100%; + padding: 20rpx 32rpx; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + background: #f5f5f5; + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06); +} +.result-share-footer-btn { + width: 100% !important; + box-sizing: border-box; + display: block; + margin: 0; + padding: 0; + min-height: 96rpx; + line-height: 96rpx; + border-radius: 999rpx; + font-size: 30rpx; + font-weight: 600; + color: #fff; + background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%); + border: none; +} +.result-share-footer-btn::after { + border: none; +} diff --git a/miniprogram/pages/result/mbti.js b/miniprogram/pages/result/mbti.js index 4a50608..4360694 100644 --- a/miniprogram/pages/result/mbti.js +++ b/miniprogram/pages/result/mbti.js @@ -30,19 +30,24 @@ Page({ shareToken: '', hasReloadedAfterPay: false, hasPhone: false, + fromShare: false }, onLoad(options) { - const id = options && options.id - const type = options && options.type + const fromShareFs = + options && (String(options.fs) === '1' || options.from === 'share') + const id = options && options.id != null && options.id !== '' ? String(options.id) : '' + const st = options && options.st ? String(options.st).trim() : '' + const type = options && options.type ? String(options.type).toLowerCase() : '' if (id && type === 'mbti') { - this.setData({ testResultId: id }) - if (options.st) { - this.loadShareDetail(id, options.st) - } else { - this.loadDetail(id) - } + this.setData({ testResultId: id, fromShare: !!fromShareFs }) + this.loadShareDetail(id, st, 'mbti') + return + } + if (id && st) { + this.setData({ testResultId: id, fromShare: true }) + this.loadShareDetail(id, st, '') return } @@ -83,10 +88,14 @@ Page({ isPaid, amountYuan: needPaymentToUnlock ? amountYuan : 0 } - this.setData({ + const patch = { payInfo, shareToken: payload.shareToken || '' - }) + } + if (payload.id != null && payload.id !== '') { + patch.testResultId = String(payload.id) + } + this.setData(patch) }, loadDetail(id) { @@ -114,17 +123,20 @@ Page({ }) }, - loadShareDetail(id, st) { + loadShareDetail(id, st, testType) { const apiBase = app.globalData?.apiBase || '' if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }) return } + const data = { id: String(id) } + if (st) data.st = st + if (testType) data.type = testType wx.showLoading({ title: '加载中...' }) wx.request({ url: `${apiBase}/api/test/share-detail`, method: 'GET', - data: { id, st }, + data, success: (res) => { if (res.statusCode === 200 && res.data && res.data.code === 200) { this.applyDetailPayload(res.data.data || {}) @@ -243,6 +255,10 @@ Page({ wx.navigateTo({ url: '/pages/test/mbti' }) }, + goWantTest() { + wx.navigateTo({ url: '/pages/test/mbti' }) + }, + goHome() { const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' if (scope === 'enterprise') { @@ -259,8 +275,7 @@ Page({ title: `我的MBTI类型是${result?.mbtiType}(${result?.description?.name}),来测测你的吧!`, path: getResultSharePath('/pages/result/mbti', { id: this.data.testResultId, - type: 'mbti', - shareToken: this.data.shareToken + type: 'mbti' }), imageUrl: '/images/share-mbti.png' } @@ -273,8 +288,7 @@ Page({ title: `我的MBTI类型是${result?.mbtiType}(${result?.description?.name}),来测测你的吧!`, query: getResultShareTimelineQuery({ id: this.data.testResultId, - type: 'mbti', - shareToken: this.data.shareToken + type: 'mbti' }) } } diff --git a/miniprogram/pages/result/mbti.wxml b/miniprogram/pages/result/mbti.wxml index 8c21893..2e47f76 100644 --- a/miniprogram/pages/result/mbti.wxml +++ b/miniprogram/pages/result/mbti.wxml @@ -1,7 +1,7 @@ - - + + 您的MBTI性格类型 @@ -42,7 +42,7 @@ - + 完整报告需完善资料 请补全头像、昵称并绑定手机号后查看完整性格分析。 @@ -114,4 +114,7 @@ --> + + + diff --git a/miniprogram/pages/result/mbti.wxss b/miniprogram/pages/result/mbti.wxss index 46c458c..9997d2f 100644 --- a/miniprogram/pages/result/mbti.wxss +++ b/miniprogram/pages/result/mbti.wxss @@ -15,6 +15,14 @@ padding-bottom: 64rpx; } +.content-container--with-fixed-footer { + padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom)); +} + +.content-scroll--with-fixed-footer { + height: 100vh; +} + .type-card, .analysis-card { background-color: #fff; @@ -247,3 +255,35 @@ } .paywall-btn-main { font-size: 30rpx; color: #fff; font-weight: 600; } .paywall-btn-price { font-size: 24rpx; color: #ffe5f0; } + +.result-share-footer { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 200; + box-sizing: border-box; + width: 100%; + padding: 20rpx 32rpx; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + background: #f5f5f5; + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06); +} +.result-share-footer-btn { + width: 100% !important; + box-sizing: border-box; + display: block; + margin: 0; + padding: 0; + min-height: 96rpx; + line-height: 96rpx; + border-radius: 999rpx; + font-size: 30rpx; + font-weight: 600; + color: #fff; + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + border: none; +} +.result-share-footer-btn::after { + border: none; +} diff --git a/miniprogram/pages/result/pdp.js b/miniprogram/pages/result/pdp.js index 5eee0b8..9e46dd0 100644 --- a/miniprogram/pages/result/pdp.js +++ b/miniprogram/pages/result/pdp.js @@ -46,18 +46,24 @@ Page({ testResultId: null, shareToken: '', hasReloadedAfterPay: false, + fromShare: false }, onLoad(options) { - const id = options && options.id - const type = options && options.type + const fromShareFs = + options && (String(options.fs) === '1' || options.from === 'share') + const id = options && options.id != null && options.id !== '' ? String(options.id) : '' + const st = options && options.st ? String(options.st).trim() : '' + const type = options && options.type ? String(options.type).toLowerCase() : '' + if (id && type === 'pdp') { - this.setData({ testResultId: id }) - if (options.st) { - this.loadShareDetail(id, options.st) - } else { - this.loadDetail(id) - } + this.setData({ testResultId: id, fromShare: !!fromShareFs }) + this.loadShareDetail(id, st, 'pdp') + return + } + if (id && st) { + this.setData({ testResultId: id, fromShare: true }) + this.loadShareDetail(id, st, '') return } const result = wx.getStorageSync('pdpResult') @@ -89,7 +95,11 @@ Page({ isPaid, amountYuan: needPaymentToUnlock ? amountYuan : 0 } - this.setData({ payInfo }) + const patch = { payInfo } + if (payload.id != null && payload.id !== '') { + patch.testResultId = String(payload.id) + } + this.setData(patch) }, loadDetail(id) { @@ -114,14 +124,17 @@ Page({ }) }, - loadShareDetail(id, st) { + loadShareDetail(id, st, testType) { const apiBase = app.globalData?.apiBase || '' if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }); return } + const data = { id: String(id) } + if (st) data.st = st + if (testType) data.type = testType wx.showLoading({ title: '加载中...' }) wx.request({ url: `${apiBase}/api/test/share-detail`, method: 'GET', - data: { id, st }, + data, success: (res) => { if (res.statusCode === 200 && res.data && res.data.code === 200) { this.applyDetailPayload(res.data.data || {}) @@ -190,6 +203,10 @@ Page({ wx.navigateTo({ url: '/pages/test/pdp' }) }, + goWantTest() { + wx.navigateTo({ url: '/pages/test/pdp' }) + }, + goHome() { const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' if (scope === 'enterprise') { @@ -206,8 +223,7 @@ Page({ title: `我的PDP类型是${line},来测测你的吧!`, path: getResultSharePath('/pages/result/pdp', { id: this.data.testResultId, - type: 'pdp', - shareToken: this.data.shareToken + type: 'pdp' }) } }, @@ -219,8 +235,7 @@ Page({ title: `我的PDP类型是${line},来测测你的吧!`, query: getResultShareTimelineQuery({ id: this.data.testResultId, - type: 'pdp', - shareToken: this.data.shareToken + type: 'pdp' }) } } diff --git a/miniprogram/pages/result/pdp.wxml b/miniprogram/pages/result/pdp.wxml index cd55370..ea10fce 100644 --- a/miniprogram/pages/result/pdp.wxml +++ b/miniprogram/pages/result/pdp.wxml @@ -1,7 +1,7 @@ - - + + PDP性格类型 @@ -26,7 +26,7 @@ - + 完整报告需完善资料 请补全头像、昵称并绑定手机号后查看完整 PDP 报告。 @@ -94,4 +94,7 @@ --> + + + diff --git a/miniprogram/pages/result/pdp.wxss b/miniprogram/pages/result/pdp.wxss index c799dbd..54e1b98 100644 --- a/miniprogram/pages/result/pdp.wxss +++ b/miniprogram/pages/result/pdp.wxss @@ -15,6 +15,14 @@ padding-bottom: 64rpx; } +.content-container--with-fixed-footer { + padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom)); +} + +.content-scroll--with-fixed-footer { + height: 100vh; +} + .type-card, .analysis-card { background-color: #fff; @@ -209,3 +217,35 @@ .paywall-btn { position: absolute; left: 5%; right: 5%; width: 90%; bottom: 132rpx; z-index: 3; padding: 20rpx 0; border-radius: 999rpx; background: linear-gradient(135deg, #F59E0B 0%, #fbbf24 100%); box-shadow: 0 8rpx 24rpx rgba(245,158,11,0.35); display: flex; flex-direction: row; align-items: baseline; justify-content: center; gap: 12rpx; } .paywall-btn-main { font-size: 30rpx; color: #fff; font-weight: 600; } .paywall-btn-price { font-size: 24rpx; color: #ffe5f0; } + +.result-share-footer { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 200; + box-sizing: border-box; + width: 100%; + padding: 20rpx 32rpx; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + background: #f5f5f5; + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06); +} +.result-share-footer-btn { + width: 100% !important; + box-sizing: border-box; + display: block; + margin: 0; + padding: 0; + min-height: 96rpx; + line-height: 96rpx; + border-radius: 999rpx; + font-size: 30rpx; + font-weight: 600; + color: #fff; + background: linear-gradient(135deg, #d97706 0%, #fbbf24 100%); + border: none; +} +.result-share-footer-btn::after { + border: none; +} diff --git a/miniprogram/pages/result/resume.js b/miniprogram/pages/result/resume.js index f204189..24ef995 100644 --- a/miniprogram/pages/result/resume.js +++ b/miniprogram/pages/result/resume.js @@ -29,12 +29,29 @@ Page({ const fileUrl = options.fileUrl ? decodeURIComponent(options.fileUrl) : '' this.setData({ fileUrl }) - // 从历史记录进入:直接读已存的数据,不重新生成 - if (options.id && options.type === 'resume') { - this.loadFromHistory(options.id) - } else { - this.fetchResumeAnalysis() + const run = () => { + if (options.id && options.type === 'resume') { + this.loadFromHistory(options.id) + } else { + this.fetchResumeAnalysis() + } } + + // 分享直达:需 token 调 detail / analyze,等静默登录完成再请求,避免白屏 + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ + loading: false, + error: '请先登录后查看报告;若已登录请下拉重试或返回重进。' + }) + return + } + run() + }) + .catch(() => { + this.setData({ loading: false, error: '登录失败,请返回重试。' }) + }) }, onShow() { diff --git a/miniprogram/pages/result/sbti.js b/miniprogram/pages/result/sbti.js index 1872c5a..0c16445 100644 --- a/miniprogram/pages/result/sbti.js +++ b/miniprogram/pages/result/sbti.js @@ -54,20 +54,28 @@ Page({ testResultId: null, shareToken: '', hasReloadedAfterPay: false, - hasPhone: false + hasPhone: false, + /** 分享落地(path 带 fs=1 或旧版仅 id+st),用于隐藏「去完善资料」、展示底部「我也要测试」 */ + fromShare: false }, onLoad(options) { - const id = options && options.id - const type = options && options.type + const fromShareFs = + options && (String(options.fs) === '1' || options.from === 'share') + const id = options && options.id != null && options.id !== '' ? String(options.id) : '' + const st = options && options.st ? String(options.st).trim() : '' + const type = options && options.type ? String(options.type).toLowerCase() : '' + // id + type:免登录拉取(/api/test/share-detail) if (id && type === 'sbti') { - this.setData({ testResultId: id }) - if (options.st) { - this.loadShareDetail(id, options.st) - } else { - this.loadDetail(id) - } + this.setData({ testResultId: id, fromShare: !!fromShareFs }) + this.loadShareDetail(id, st, 'sbti') + return + } + // 旧版分享仅 id + st + if (id && st) { + this.setData({ testResultId: id, fromShare: true }) + this.loadShareDetail(id, st, '') return } @@ -108,10 +116,14 @@ Page({ isPaid, amountYuan: needPaymentToUnlock ? amountYuan : 0 } - this.setData({ + const patch = { payInfo, shareToken: payload.shareToken || '' - }) + } + if (payload.id != null && payload.id !== '') { + patch.testResultId = String(payload.id) + } + this.setData(patch) }, loadDetail(id) { @@ -139,17 +151,20 @@ Page({ }) }, - loadShareDetail(id, st) { + loadShareDetail(id, st, testType) { const apiBase = app.globalData?.apiBase || '' if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }) return } + const data = { id: String(id) } + if (st) data.st = st + if (testType) data.type = testType wx.showLoading({ title: '加载中...' }) wx.request({ url: `${apiBase}/api/test/share-detail`, method: 'GET', - data: { id, st }, + data, success: (res) => { if (res.statusCode === 200 && res.data && res.data.code === 200) { this.applyDetailPayload(res.data.data || {}) @@ -262,6 +277,11 @@ Page({ wx.navigateTo({ url: '/pages/test/sbti' }) }, + /** 分享落地页:引导好友自己做测试 */ + goWantTest() { + wx.navigateTo({ url: '/pages/test/sbti' }) + }, + goHome() { const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' if (scope === 'enterprise') { @@ -281,8 +301,7 @@ Page({ title: `我的 SBTI 类型是 ${code}(${label}),来测测你的吧!`, path: getResultSharePath('/pages/result/sbti', { id: this.data.testResultId, - type: 'sbti', - shareToken: this.data.shareToken + type: 'sbti' }), imageUrl: img } @@ -297,8 +316,7 @@ Page({ title: `我的 SBTI 类型是 ${code}(${label}),来测测你的吧!`, query: getResultShareTimelineQuery({ id: this.data.testResultId, - type: 'sbti', - shareToken: this.data.shareToken + type: 'sbti' }) } } diff --git a/miniprogram/pages/result/sbti.wxml b/miniprogram/pages/result/sbti.wxml index 5ed7af9..0e84eac 100644 --- a/miniprogram/pages/result/sbti.wxml +++ b/miniprogram/pages/result/sbti.wxml @@ -1,7 +1,7 @@ - - + + @@ -72,8 +72,8 @@ - - + + 完整报告需完善资料 请补全头像、昵称并绑定手机号后查看完整解读。 @@ -107,4 +107,7 @@ + + + \ No newline at end of file diff --git a/miniprogram/pages/result/sbti.wxss b/miniprogram/pages/result/sbti.wxss index c2d7f71..5e30857 100644 --- a/miniprogram/pages/result/sbti.wxss +++ b/miniprogram/pages/result/sbti.wxss @@ -15,6 +15,15 @@ padding-bottom: 64rpx; } +/* 底部固定「我也要测试」时,为滚动内容预留空间(按钮约 96rpx + 安全区) */ +.content-container--with-fixed-footer { + padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom)); +} + +.content-scroll--with-fixed-footer { + height: 100vh; +} + .card { background-color: #fff; border-radius: 24rpx; @@ -268,3 +277,40 @@ font-weight: 400; line-height: 1.65; } + +/* 分享落地:底部固定条 */ +.result-share-footer { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 200; + box-sizing: border-box; + width: 100%; + padding: 20rpx 32rpx; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + background: #f2f7f3; + box-shadow: 0 -8rpx 24rpx rgba(45, 61, 53, 0.08); +} + +/* 与 SBTI 答题页主按钮(pages/test/sbti)主题绿一致 */ +.result-share-footer-btn { + width: 100% !important; + box-sizing: border-box; + display: block; + margin: 0; + padding: 0; + min-height: 96rpx; + line-height: 96rpx; + border-radius: 999rpx; + font-size: 30rpx; + font-weight: 600; + color: #fff; + background: linear-gradient(135deg, #5a7268 0%, #6f8a7e 100%); + border: none; + box-shadow: 0 8rpx 24rpx rgba(90, 114, 104, 0.28); +} + +.result-share-footer-btn::after { + border: none; +} diff --git a/miniprogram/pages/test-select/index.js b/miniprogram/pages/test-select/index.js index 1e6862f..dc46329 100644 --- a/miniprogram/pages/test-select/index.js +++ b/miniprogram/pages/test-select/index.js @@ -7,7 +7,9 @@ Page({ permMbti: true, permSbti: true, permPdp: true, - permDisc: true + permDisc: true, + /** 四类入口均被企业权限关闭时提示,避免误以为白屏 */ + allTestsDisabled: false }, onLoad(options) { @@ -17,6 +19,8 @@ Page({ try { wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] }) } catch (e) {} + // 分享直达时尽早完成静默登录,避免用户点进子页时仍无 token + app.ensureLogin().catch(() => {}) this._syncPerms() }, @@ -26,12 +30,18 @@ Page({ _syncPerms() { const p = app.globalData.enterprisePermissions + const permFace = !p || p.face !== false + const permMbti = !p || p.mbti !== false + const permSbti = !p || p.sbti !== false + const permPdp = !p || p.pdp !== false + const permDisc = !p || p.disc !== false this.setData({ - permFace: !p || p.face !== false, - permMbti: !p || p.mbti !== false, - permSbti: !p || p.sbti !== false, - permPdp: !p || p.pdp !== false, - permDisc: !p || p.disc !== false + permFace, + permMbti, + permSbti, + permPdp, + permDisc, + allTestsDisabled: p && !permMbti && !permSbti && !permPdp && !permDisc }) }, diff --git a/miniprogram/pages/test-select/index.wxml b/miniprogram/pages/test-select/index.wxml index 64bcd17..9545917 100644 --- a/miniprogram/pages/test-select/index.wxml +++ b/miniprogram/pages/test-select/index.wxml @@ -48,4 +48,8 @@ + + + 当前环境下暂未开放问卷测试,请联系管理员或从首页进入。 + diff --git a/miniprogram/pages/test-select/index.wxss b/miniprogram/pages/test-select/index.wxss index df42e79..db42da5 100644 --- a/miniprogram/pages/test-select/index.wxss +++ b/miniprogram/pages/test-select/index.wxss @@ -35,6 +35,17 @@ line-height: 1.6; } +.perm-disabled-hint { + text-align: center; + padding: 32rpx; +} + +.perm-disabled-text { + font-size: 28rpx; + color: #888; + line-height: 1.6; +} + /* 入口卡片(与 result 页 next-btn 同色系) */ .entry-card { display: flex; diff --git a/miniprogram/pages/test/disc.js b/miniprogram/pages/test/disc.js index 8eda201..5d98b7d 100644 --- a/miniprogram/pages/test/disc.js +++ b/miniprogram/pages/test/disc.js @@ -28,8 +28,17 @@ Page({ try { require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app) } catch (e) {} - loadQuestions('disc', {}) + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ loading: false }) + wx.showToast({ title: '登录失败,请重试', icon: 'none' }) + return Promise.reject(new Error('login')) + } + return loadQuestions('disc', {}) + }) .then((questions) => { + if (!questions) return if (!questions.length) { wx.showToast({ title: '暂无题目', icon: 'none' }) this.setData({ loading: false }) @@ -53,6 +62,7 @@ Page({ this.startTimer() }) .catch((err) => { + if (err && err.message === 'login') return this.setData({ loading: false }) wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }) }) @@ -189,10 +199,17 @@ Page({ wx.setStorageSync('discResult', resultData) try { require('../../utils/analytics').track('test_complete', { type: 'disc', result: dominantType, duration: resultData.testDuration }) } catch (e) {} if (app && typeof app.saveTestResult === 'function') { - app.saveTestResult('disc', resultData) + app.saveTestResult('disc', resultData).then((extra) => { + const rid = extra && extra.id + if (rid) { + wx.redirectTo({ url: `/pages/result/disc?id=${rid}&type=disc` }) + } else { + wx.redirectTo({ url: '/pages/result/disc' }) + } + }) + } else { + wx.redirectTo({ url: '/pages/result/disc' }) } - - wx.redirectTo({ url: '/pages/result/disc' }) }, onShareAppMessage() { diff --git a/miniprogram/pages/test/mbti.js b/miniprogram/pages/test/mbti.js index bf22a46..ea51cd8 100644 --- a/miniprogram/pages/test/mbti.js +++ b/miniprogram/pages/test/mbti.js @@ -30,8 +30,18 @@ Page({ try { require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app) } catch (e) {} - loadQuestions('mbti', {}) + // 分享直达本页时 silentLogin 可能尚未完成,须先 ensureLogin 再拉题,否则 401 → 白屏 + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ loading: false }) + wx.showToast({ title: '登录失败,请重试', icon: 'none' }) + return Promise.reject(new Error('login')) + } + return loadQuestions('mbti', {}) + }) .then((questions) => { + if (!questions) return const total = questions.length if (!total) { wx.showToast({ title: '暂无题目', icon: 'none' }) @@ -58,6 +68,7 @@ Page({ this.startTimer() }) .catch((err) => { + if (err && err.message === 'login') return this.setData({ loading: false }) wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }) }) @@ -229,11 +240,15 @@ Page({ timestamp: new Date().toISOString() } wx.setStorageSync('mbtiResult', resultData) - app.saveTestResult('mbti', resultData) try { require('../../utils/analytics').track('test_complete', { type: 'mbti', result: result.mbtiType, confidence: result.confidence, duration: resultData.testDuration }) } catch (e) {} - wx.redirectTo({ - url: '/pages/result/mbti' + app.saveTestResult('mbti', resultData).then((extra) => { + const rid = extra && extra.id + if (rid) { + wx.redirectTo({ url: `/pages/result/mbti?id=${rid}&type=mbti` }) + } else { + wx.redirectTo({ url: '/pages/result/mbti' }) + } }) }, diff --git a/miniprogram/pages/test/pdp.js b/miniprogram/pages/test/pdp.js index d771962..3585fc8 100644 --- a/miniprogram/pages/test/pdp.js +++ b/miniprogram/pages/test/pdp.js @@ -28,8 +28,17 @@ Page({ try { require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app) } catch (e) {} - loadQuestions('pdp', {}) + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ loading: false }) + wx.showToast({ title: '登录失败,请重试', icon: 'none' }) + return Promise.reject(new Error('login')) + } + return loadQuestions('pdp', {}) + }) .then((questions) => { + if (!questions) return if (!questions.length) { wx.showToast({ title: '暂无题目', icon: 'none' }) this.setData({ loading: false }) @@ -53,6 +62,7 @@ Page({ this.startTimer() }) .catch((err) => { + if (err && err.message === 'login') return this.setData({ loading: false }) wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }) }) @@ -192,10 +202,17 @@ Page({ wx.setStorageSync('pdpResult', resultData) try { require('../../utils/analytics').track('test_complete', { type: 'pdp', result: dominantType, duration: resultData.testDuration }) } catch (e) {} if (app && typeof app.saveTestResult === 'function') { - app.saveTestResult('pdp', resultData) + app.saveTestResult('pdp', resultData).then((extra) => { + const rid = extra && extra.id + if (rid) { + wx.redirectTo({ url: `/pages/result/pdp?id=${rid}&type=pdp` }) + } else { + wx.redirectTo({ url: '/pages/result/pdp' }) + } + }) + } else { + wx.redirectTo({ url: '/pages/result/pdp' }) } - - wx.redirectTo({ url: '/pages/result/pdp' }) }, onShareAppMessage() { diff --git a/miniprogram/pages/test/sbti.js b/miniprogram/pages/test/sbti.js index ea140c1..fb6f6bc 100644 --- a/miniprogram/pages/test/sbti.js +++ b/miniprogram/pages/test/sbti.js @@ -27,9 +27,18 @@ Page({ try { require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app) } catch (e) {} - fetchQuestionBank('sbti', {}) + app.ensureLogin() + .then((ok) => { + if (!ok) { + this.setData({ loading: false }) + wx.showToast({ title: '登录失败,请重试', icon: 'none' }) + return Promise.reject(new Error('login')) + } + return fetchQuestionBank('sbti', {}) + }) .then((all) => { - if (!all || !all.length) { + if (!all) return + if (!all.length) { wx.showToast({ title: '暂无题目', icon: 'none' }) this.setData({ loading: false }) return @@ -62,6 +71,7 @@ Page({ this.startTimer() }) .catch((err) => { + if (err && err.message === 'login') return this.setData({ loading: false }) wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }) }) @@ -236,7 +246,6 @@ Page({ timestamp: new Date().toISOString() } wx.setStorageSync('sbtiResult', resultData) - app.saveTestResult('sbti', resultData) try { require('../../utils/analytics').track('test_complete', { type: 'sbti', @@ -245,8 +254,13 @@ Page({ }) } catch (e) {} - wx.redirectTo({ - url: '/pages/result/sbti' + app.saveTestResult('sbti', resultData).then((extra) => { + const rid = extra && extra.id + if (rid) { + wx.redirectTo({ url: `/pages/result/sbti?id=${rid}&type=sbti` }) + } else { + wx.redirectTo({ url: '/pages/result/sbti' }) + } }) }, diff --git a/miniprogram/project.private.config.json b/miniprogram/project.private.config.json index 334a8bf..e5bb153 100644 --- a/miniprogram/project.private.config.json +++ b/miniprogram/project.private.config.json @@ -7,12 +7,19 @@ "condition": { "miniprogram": { "list": [ + { + "name": "分享的测试数据", + "pathName": "pages/result/sbti", + "query": "id=6229&type=sbti&uid=24&fs=1", + "scene": null, + "launchMode": "default" + }, { "name": "MBTI", "pathName": "pages/test/mbti", "query": "phone=18649947301", - "scene": null, - "launchMode": "default" + "launchMode": "default", + "scene": null }, { "name": "pages/enterprise/index", diff --git a/miniprogram/utils/share.js b/miniprogram/utils/share.js index 4f8a42a..28295e1 100644 --- a/miniprogram/utils/share.js +++ b/miniprogram/utils/share.js @@ -58,24 +58,23 @@ function getSharePathByScope(personalBasePath) { } /** - * 分享「测试结果页」:必须带 id、type、st(后端下发的 shareToken),否则好友无法打开详情 + * 分享「测试结果页」:仅需 id + type(好友免登录,由 /api/test/share-detail 按 type 校验) * @param {string} resultPagePath 如 /pages/result/mbti - * @param {{ id: string|number, type: string, shareToken: string }} opts + * @param {{ id: string|number, type: string }} opts */ function getResultSharePath(resultPagePath, opts) { const id = opts && opts.id const type = opts && opts.type - const shareToken = opts && opts.shareToken - if (!id || !type || !shareToken) { + if (!id || !type) { return getSharePathByScope('/pages/index/index') } + // fs=1:分享落地,结果页用于区分访客(隐藏「去完善资料」、展示「我也要测试」) const q = 'id=' + encodeURIComponent(String(id)) + '&type=' + encodeURIComponent(String(type)) + - '&st=' + - encodeURIComponent(String(shareToken)) + '&fs=1' const inv = buildShareQuery() return inv ? resultPagePath + '?' + q + '&' + inv : resultPagePath + '?' + q } @@ -86,8 +85,7 @@ function getResultSharePath(resultPagePath, opts) { function getResultShareTimelineQuery(opts) { const id = opts && opts.id const type = opts && opts.type - const shareToken = opts && opts.shareToken - if (!id || !type || !shareToken) { + if (!id || !type) { return buildShareQuery() } const base = @@ -95,8 +93,7 @@ function getResultShareTimelineQuery(opts) { encodeURIComponent(String(id)) + '&type=' + encodeURIComponent(String(type)) + - '&st=' + - encodeURIComponent(String(shareToken)) + '&fs=1' const inv = buildShareQuery() return inv ? base + '&' + inv : base }