From 41f016b1ab375140f2fee088dcd90cd6ab5ac9a9 Mon Sep 17 00:00:00 2001 From: Ghost <106998207@qq.com> Date: Tue, 24 Mar 2026 10:07:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=AE=BE=E7=BD=AE=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E4=BC=81=E4=B8=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/views/superadmin/Settings.vue | 46 +++++ api/app/controller/api/Analyze.php | 13 +- api/app/controller/api/AppConfig.php | 35 +++- api/app/controller/api/Payment.php | 5 + api/app/controller/api/Test.php | 106 ++++++++++- api/app/controller/superadmin/Settings.php | 32 +++- miniprogram/app.js | 24 ++- miniprogram/pages/enterprise/index.js | 9 +- .../pages/enterprise/resume-history.js | 5 +- miniprogram/pages/history/index.js | 6 +- miniprogram/pages/index/result.js | 11 +- miniprogram/pages/index/result.wxml | 174 +++++++++--------- miniprogram/pages/index/result.wxss | 150 +++++++++++++++ miniprogram/pages/promo/poster.js | 5 +- miniprogram/pages/recharge/index.js | 23 ++- miniprogram/pages/result/disc.wxss | 8 +- miniprogram/pages/result/mbti.wxss | 23 ++- miniprogram/pages/result/pdp.wxss | 8 +- miniprogram/pages/result/resume.js | 9 +- miniprogram/utils/enterpriseContext.js | 52 ++++++ miniprogram/utils/payment.js | 10 + miniprogram/utils/share.js | 6 +- 22 files changed, 594 insertions(+), 166 deletions(-) create mode 100644 miniprogram/utils/enterpriseContext.js diff --git a/admin/src/views/superadmin/Settings.vue b/admin/src/views/superadmin/Settings.vue index ab7e54c..09642bb 100644 --- a/admin/src/views/superadmin/Settings.vue +++ b/admin/src/views/superadmin/Settings.vue @@ -90,6 +90,24 @@ class="form-input" /> +
+ + + + + 未带企业入口参数(如 scene / 链接中的 eid)时,小程序回落使用该企业的上下文;优先级:扫码或链接中的企业 > 用户已绑定企业 > 此处默认企业 +
@@ -390,8 +408,12 @@ const systemConfig = reactive({ maintenanceMode: false, maxTestsPerDay: 100, trialTestCount: 10, + defaultEnterpriseId: null as number | null, }) +/** 下拉:企业管理中的企业列表 */ +const enterpriseOptions = ref>([]) + // 超管凭据 const credentials = reactive({ username: 'admin', @@ -432,6 +454,9 @@ const loadSettings = async () => { // 加载系统配置 if (response.data.system) { Object.assign(systemConfig, response.data.system) + const de = response.data.system.defaultEnterpriseId + systemConfig.defaultEnterpriseId = + de != null && de !== '' && Number(de) > 0 ? Number(de) : null } // 加载小程序文案配置 if (response.data.textConfig && typeof response.data.textConfig === 'object') { @@ -468,8 +493,21 @@ const loadSettings = async () => { } } +async function loadEnterpriseOptions() { + try { + const res: any = await request.get('/enterprises', { params: { page: 1, pageSize: 500 } }) + const list = (res?.code === 200 && res?.data?.list) ? res.data.list : [] + enterpriseOptions.value = Array.isArray(list) + ? list.map((r: any) => ({ id: Number(r.id), name: String(r.name || '') })) + : [] + } catch { + enterpriseOptions.value = [] + } +} + onMounted(() => { loadSettings() + loadEnterpriseOptions() }) // 保存配置 @@ -691,6 +729,14 @@ const handleSave = async (section: string) => { @media (min-width: 768px) { grid-template-columns: repeat(2, 1fr); } + + .form-item-full { + grid-column: 1 / -1; + } +} + +.w-full { + width: 100%; } .form-section { diff --git a/api/app/controller/api/Analyze.php b/api/app/controller/api/Analyze.php index 7fc4ac2..3100ddd 100644 --- a/api/app/controller/api/Analyze.php +++ b/api/app/controller/api/Analyze.php @@ -202,10 +202,15 @@ class Analyze extends BaseController 'amountYuan' => $standardAmountFen > 0 ? round($standardAmountFen / 100, 2) : 0, ]; - // 若该类型需付费才显示完整报告:返回给前端的也做脱敏,与详情/历史一致(只显示部分,付费后从详情接口拿完整) - if ($requiresPayment) { - $responsePayload['faceAnalysis'] = null; - $responsePayload['boneAnalysis'] = null; + // 未付费 / 资料未完善:与 /api/test/detail 一致做预览脱敏,防止直接抓包拿到完整报告 + $gateResponse = false; + if ($requiresPayment > 0) { + $gateResponse = true; + } elseif ($earlyUserId > 0 && !TestController::isWechatProfileComplete($earlyUserId)) { + $gateResponse = true; + } + if (is_array($responsePayload) && $gateResponse) { + $responsePayload = TestController::filterFaceResultToPreview($responsePayload); } // 把本次测试记录ID一起返回,便于解锁后通过 /api/test/detail 拉取完整数据 diff --git a/api/app/controller/api/AppConfig.php b/api/app/controller/api/AppConfig.php index beb2923..159f6e8 100644 --- a/api/app/controller/api/AppConfig.php +++ b/api/app/controller/api/AppConfig.php @@ -50,6 +50,30 @@ class AppConfig extends BaseController } } + // 系统配置:审核模式、默认企业(无带参入口时小程序回落) + $maintenanceMode = false; + $defaultEnterpriseId = null; + $systemRow = Db::name('system_config')->where('key', 'system')->find(); + if ($systemRow && !empty($systemRow['value'])) { + $sysValEarly = is_string($systemRow['value']) ? json_decode($systemRow['value'], true) : $systemRow['value']; + if (is_array($sysValEarly)) { + if (!empty($sysValEarly['maintenanceMode'])) { + $maintenanceMode = true; + } + if (!empty($sysValEarly['defaultEnterpriseId'])) { + $de = (int) $sysValEarly['defaultEnterpriseId']; + if ($de > 0) { + $defaultEnterpriseId = $de; + } + } + } + } + // scope=enterprise 且用户未绑定企业时,用超管默认企业拉企业定价与文案 + if ($scope !== 'personal' && $enterpriseId === null && $defaultEnterpriseId !== null) { + $enterpriseId = $defaultEnterpriseId; + $pricingType = 'enterprise'; + } + $config = PricingConfigModel::getByTypeAndEnterprise($pricingType, $enterpriseId); if ($config && !empty($config->config)) { // PricingConfig 模型已对 config 做 JSON 转换,这里直接当数组/对象用即可 @@ -103,16 +127,6 @@ class AppConfig extends BaseController } $siteTitle = $miniprogramName !== '' ? $miniprogramName : ($siteName !== '' ? $siteName : '神仙团队AI性格测试'); - // 审核模式(原 maintenanceMode):开启后首页展示「开始性格测试」并跳转 test-select - $maintenanceMode = false; - $systemRow = Db::name('system_config')->where('key', 'system')->find(); - if ($systemRow && !empty($systemRow['value'])) { - $sysVal = is_string($systemRow['value']) ? json_decode($systemRow['value'], true) : $systemRow['value']; - if (is_array($sysVal) && !empty($sysVal['maintenanceMode'])) { - $maintenanceMode = true; - } - } - // 小程序文案配置(分析中提示、按钮、报告标题等) $textConfig = [ 'analyzingTitle' => '正在分析中', @@ -155,6 +169,7 @@ class AppConfig extends BaseController 'siteTitle' => $siteTitle, 'textConfig' => $textConfig, 'maintenanceMode' => $maintenanceMode, + 'defaultEnterpriseId' => $defaultEnterpriseId, ]); } diff --git a/api/app/controller/api/Payment.php b/api/app/controller/api/Payment.php index f46129e..c0771d4 100644 --- a/api/app/controller/api/Payment.php +++ b/api/app/controller/api/Payment.php @@ -98,6 +98,11 @@ class Payment extends BaseController } } + // 测试结果未带 enterpriseId 时,使用小程序请求中的企业上下文(scene/绑定/超管默认企业) + if (empty($enterpriseId) && $enterpriseIdParam > 0) { + $enterpriseId = $enterpriseIdParam; + } + // 充值场景:优先使用显式传入的企业ID,否则回退到当前用户已绑定企业 if ($productType === 'recharge') { if ($enterpriseIdParam > 0) { diff --git a/api/app/controller/api/Test.php b/api/app/controller/api/Test.php index d6cb8f9..816f3ab 100644 --- a/api/app/controller/api/Test.php +++ b/api/app/controller/api/Test.php @@ -80,6 +80,9 @@ class Test extends BaseController $requiresPayment = (int) ($row['requiresPayment'] ?? 0); $isPaid = (int) ($row['isPaid'] ?? 0); $orderId = isset($row['orderId']) ? (int) $row['orderId'] : null; + $paidAmountRow = isset($row['paidAmount']) ? (int) $row['paidAmount'] : 0; + $needPayUnlock = $requiresPayment && !$isPaid && $paidAmountRow > 0; + $profileIncomplete = !self::isWechatProfileComplete($userId); $raw = $row['resultData'] ?? ($row['result'] ?? null); $data = null; @@ -87,8 +90,14 @@ class Test extends BaseController $decoded = json_decode($raw, true); $data = is_array($decoded) ? $decoded : $raw; } - if ($requiresPayment && !$isPaid && $data !== null) { - $data = $this->filterResultToPartial($testType, $data); + if ($data !== null && is_array($data)) { + if (in_array($testType, ['face', 'ai'], true)) { + if ($needPayUnlock || $profileIncomplete) { + $data = self::filterFaceResultToPreview($data); + } + } elseif ($requiresPayment && !$isPaid) { + $data = $this->filterResultToPartial($testType, $data); + } } $paymentFields = [ @@ -358,8 +367,15 @@ class Test extends BaseController $testType = $row['testType'] ?? ''; // 仅当需要付款且未付款且金额>0 时才脱敏;系统设置需付款但金额为0 则直接可查看 $needPaymentToUnlock = $requiresPayment && !$isPaid && $paidAmount > 0; - if ($needPaymentToUnlock && $data !== null) { - $data = $this->filterResultToPartial($testType, $data); + $profileIncomplete = !self::isWechatProfileComplete($userId); + if ($data !== null && is_array($data)) { + if (in_array($testType, ['face', 'ai'], true)) { + if ($needPaymentToUnlock || $profileIncomplete) { + $data = self::filterFaceResultToPreview($data); + } + } elseif ($needPaymentToUnlock) { + $data = $this->filterResultToPartial($testType, $data); + } } return success([ @@ -550,12 +566,6 @@ class Test extends BaseController if (!is_array($data)) { return $data; } - if ($testType === 'face' || $testType === 'ai') { - $out = $data; - $out['faceAnalysis'] = null; - $out['boneAnalysis'] = null; - return $out; - } if ($testType === 'mbti') { return [ 'mbtiType' => $data['mbtiType'] ?? $data['mbti'] ?? '', @@ -577,6 +587,82 @@ class Test extends BaseController return $data; } + /** + * 微信用户资料是否与小程序 isProfileComplete 一致:头像、昵称、手机号必填 + */ + public static function isWechatProfileComplete(int $userId): bool + { + if ($userId <= 0) { + return false; + } + $row = Db::name('wechat_users')->where('id', $userId)->field('nickname,avatar,phone')->find(); + if (!$row) { + return false; + } + $nick = trim((string) ($row['nickname'] ?? '')); + $avatar = trim((string) ($row['avatar'] ?? '')); + $phone = trim((string) ($row['phone'] ?? '')); + + return $nick !== '' && $avatar !== '' && $phone !== ''; + } + + /** + * 人脸/AI 分析报告:未付费或资料未完善时仅返回预览级字段(防止抓包看全文) + */ + public static function filterFaceResultToPreview(array $data): array + { + $out = $data; + $out['faceAnalysis'] = null; + $out['boneAnalysis'] = null; + unset($out['faceAnalysisText'], $out['boneAnalysisText']); + $out['relationship'] = ''; + $out['portrait'] = null; + $out['hrView'] = null; + $out['bossView'] = null; + $out['resumeHighlights'] = ''; + if (isset($out['careers'])) { + $out['careers'] = []; + } + + $sum = (string) ($out['personalitySummary'] ?? ''); + $ov = (string) ($out['overview'] ?? ''); + $out['personalitySummary'] = self::truncatePreviewText($sum, 72); + $out['overview'] = self::truncatePreviewText($ov, 72); + $out['gallupTop3'] = []; + + $adv = $out['advantages'] ?? []; + if (is_array($adv) && $adv !== []) { + $slice = array_slice($adv, 0, 2); + $out['advantages'] = array_map(function ($x) { + return self::truncatePreviewText((string) $x, 28); + }, $slice); + } else { + $out['advantages'] = []; + } + + return $out; + } + + private static function truncatePreviewText(string $s, int $maxChars): string + { + $s = trim($s); + if ($s === '') { + return ''; + } + if (function_exists('mb_strlen') && function_exists('mb_substr')) { + if (mb_strlen($s, 'UTF-8') > $maxChars) { + return mb_substr($s, 0, $maxChars, 'UTF-8') . '…'; + } + + return $s; + } + if (strlen($s) > $maxChars) { + return substr($s, 0, $maxChars) . '…'; + } + + return $s; + } + /** * 获取当前用户最近的 MBTI / DISC / PDP 测试记录(暂不使用人脸/AI 结果),供简历综合分析使用 * @param int $userId 微信用户 ID diff --git a/api/app/controller/superadmin/Settings.php b/api/app/controller/superadmin/Settings.php index 755755b..29a3afd 100644 --- a/api/app/controller/superadmin/Settings.php +++ b/api/app/controller/superadmin/Settings.php @@ -4,6 +4,7 @@ namespace app\controller\superadmin; use app\BaseController; use app\model\SystemConfig as SystemConfigModel; use app\model\User as UserModel; +use app\model\Enterprise as EnterpriseModel; use think\facade\Request; use think\facade\Db; @@ -55,7 +56,8 @@ class Settings extends BaseController 'miniprogramName' => '神仙团队AI性格测试', 'maintenanceMode' => false, 'maxTestsPerDay' => 100, - 'trialTestCount' => 10 + 'trialTestCount' => 10, + 'defaultEnterpriseId' => null, ], 'notification' => $notificationConfig ? $notificationConfig->value : [ 'emailNotification' => true, @@ -100,12 +102,30 @@ class Settings extends BaseController $input = []; } - $allowedKeys = ['siteName', 'siteDescription', 'miniprogramName', 'maintenanceMode', 'maxTestsPerDay', 'trialTestCount']; + $allowedKeys = ['siteName', 'siteDescription', 'miniprogramName', 'maintenanceMode', 'maxTestsPerDay', 'trialTestCount', 'defaultEnterpriseId']; $data = array_intersect_key($input, array_flip($allowedKeys)); // 兼容 fallback:JSON 解析失败时尝试 Request::only if (empty($data)) { $data = Request::only($allowedKeys); } + // 默认企业:0 或空视为不启用 + if (array_key_exists('defaultEnterpriseId', $data)) { + $de = $data['defaultEnterpriseId']; + if ($de === '' || $de === null) { + $data['defaultEnterpriseId'] = null; + } else { + $deInt = (int) $de; + if ($deInt <= 0) { + $data['defaultEnterpriseId'] = null; + } else { + $exists = EnterpriseModel::where('id', $deInt)->find(); + if (!$exists) { + return error('所选默认企业不存在', 400); + } + $data['defaultEnterpriseId'] = $deInt; + } + } + } $textConfig = $input['textConfig'] ?? (Request::param('textConfig') ?: []); try { @@ -117,7 +137,13 @@ class Settings extends BaseController $config->enterprise_id = 0; $config->description = '系统基础配置'; } - $config->value = $data; + // 合并保存,避免仅提交部分字段时丢失其它键(如 defaultEnterpriseId) + $oldVal = $config->value; + $oldArr = is_array($oldVal) ? $oldVal : (is_string($oldVal) ? (json_decode($oldVal, true) ?: []) : []); + if (!is_array($oldArr)) { + $oldArr = []; + } + $config->value = array_merge($oldArr, $data); $config->save(); // 更新站点信息 diff --git a/miniprogram/app.js b/miniprogram/app.js index 18196ab..3b07c10 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -13,6 +13,8 @@ App({ appScope: 'personal', // 扫码进入企业页时 scene 解析出的企业ID(e_123),提交测试/分析时优先使用 enterpriseIdFromScene: null, + // 超管配置的默认企业 ID(无 scene/eid 等入口参数时回落) + defaultEnterpriseId: null, // API基础地址(开发时用本地,生产环境替换为实际域名) apiBase: 'https://mbtiapi.quwanzhi.com', //apiBase: 'http://mbti.com', @@ -41,6 +43,11 @@ App({ if (cfg) { if (cfg.siteTitle) this.globalData.siteTitle = cfg.siteTitle if (cfg.maintenanceMode !== undefined) this.globalData.maintenanceMode = !!cfg.maintenanceMode + if (cfg.defaultEnterpriseId != null && Number(cfg.defaultEnterpriseId) > 0) { + this.globalData.defaultEnterpriseId = Number(cfg.defaultEnterpriseId) + } else { + this.globalData.defaultEnterpriseId = null + } } }).catch(() => {}) }, @@ -275,20 +282,16 @@ App({ }) }, - // 保存测试结果 + // 保存测试结果 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 scope = this.globalData.appScope || 'personal' - const storedUser = wx.getStorageSync('userInfo') || null - const enterpriseId = - scope === 'enterprise' - ? (this.globalData.enterpriseIdFromScene || (this.globalData.userInfo && this.globalData.userInfo.enterpriseId) || (storedUser && storedUser.enterpriseId) || null) - : null + const enterpriseId = getEnterpriseIdForApiPayload() wx.request({ url: `${this.globalData.apiBase}/api/test/submit`, method: 'POST', @@ -301,7 +304,7 @@ App({ answers: result.answers || [], result: result, userId: this.globalData.userInfo?.id ?? this.globalData.openId, - enterpriseId: enterpriseId || undefined, + enterpriseId: enterpriseId != null ? enterpriseId : undefined, testDuration: result.testDuration || 0, timestamp: new Date().toISOString() } @@ -335,6 +338,11 @@ App({ if (data.siteTitle) this.globalData.siteTitle = data.siteTitle if (data.textConfig) this.globalData.textConfig = data.textConfig if (data.maintenanceMode !== undefined) this.globalData.maintenanceMode = !!data.maintenanceMode + if (data.defaultEnterpriseId != null && Number(data.defaultEnterpriseId) > 0) { + this.globalData.defaultEnterpriseId = Number(data.defaultEnterpriseId) + } else { + this.globalData.defaultEnterpriseId = null + } resolve(data) } else { reject(new Error(res.data && res.data.message ? res.data.message : '获取配置失败')) diff --git a/miniprogram/pages/enterprise/index.js b/miniprogram/pages/enterprise/index.js index 9242589..a671e8e 100644 --- a/miniprogram/pages/enterprise/index.js +++ b/miniprogram/pages/enterprise/index.js @@ -1,6 +1,7 @@ // pages/enterprise/index.js - 企业版首页 const app = getApp() const { request } = require('../../utils/request') +const { getEffectiveEnterpriseId } = require('../../utils/enterpriseContext.js') Page({ data: { @@ -70,9 +71,9 @@ Page({ maintenanceMode }) - // 未绑定企业的用户:若从邀请码扫码进入(有 enterpriseIdFromScene)也允许使用企业版 + // 未绑定企业的用户:扫码带 eid 或超管配置了默认企业时也允许使用企业版 const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {} - const fromInvite = !!app.globalData.enterpriseIdFromScene + const fromInvite = !!app.globalData.enterpriseIdFromScene || !!(app.globalData.defaultEnterpriseId && Number(app.globalData.defaultEnterpriseId) > 0) const redirectBack = () => { wx.showToast({ title: '您尚未绑定任何企业,无法使用企业版', icon: 'none', duration: 2500 }) setTimeout(() => wx.switchTab({ url: '/pages/index/index' }), 600) @@ -119,7 +120,7 @@ Page({ maintenanceMode }) } - if ((cfg && cfg.pricingType) !== 'enterprise' && !app.globalData.enterpriseIdFromScene) { + if ((cfg && cfg.pricingType) !== 'enterprise' && !app.globalData.enterpriseIdFromScene && !(app.globalData.defaultEnterpriseId && Number(app.globalData.defaultEnterpriseId) > 0)) { redirectBack() return } @@ -176,7 +177,7 @@ Page({ wx.navigateTo({ url: '/pages/test-select/index' }) return } - const eid = (app.globalData && app.globalData.enterpriseIdFromScene) || (app.globalData && app.globalData.userInfo && app.globalData.userInfo.enterpriseId) || (wx.getStorageSync('userInfo') || {}).enterpriseId || null + const eid = getEffectiveEnterpriseId() const query = eid ? `?enterpriseId=${eid}&pageSize=1` : '?pageSize=1' request({ url: '/api/enterprise/resume-uploads' + query, diff --git a/miniprogram/pages/enterprise/resume-history.js b/miniprogram/pages/enterprise/resume-history.js index cfdc18c..f5e4f28 100644 --- a/miniprogram/pages/enterprise/resume-history.js +++ b/miniprogram/pages/enterprise/resume-history.js @@ -1,5 +1,6 @@ // pages/enterprise/resume-history.js - 简历上传记录(历史) const app = getApp() +const { getEffectiveEnterpriseId } = require('../../utils/enterpriseContext.js') const { request } = require('../../utils/request') Page({ @@ -21,7 +22,7 @@ Page({ loadList() { this.setData({ loading: true }) - const eid = (app.globalData && app.globalData.enterpriseIdFromScene) || (app.globalData && app.globalData.userInfo && app.globalData.userInfo.enterpriseId) || (wx.getStorageSync('userInfo') || {}).enterpriseId || null + const eid = getEffectiveEnterpriseId() const query = eid ? `?enterpriseId=${eid}&pageSize=100` : '?pageSize=100' request({ url: '/api/enterprise/resume-uploads' + query, @@ -48,7 +49,7 @@ Page({ }, uploadResume() { - const eid = (app.globalData && app.globalData.enterpriseIdFromScene) || (app.globalData && app.globalData.userInfo && app.globalData.userInfo.enterpriseId) || (wx.getStorageSync('userInfo') || {}).enterpriseId || null + const eid = getEffectiveEnterpriseId() wx.chooseMessageFile({ count: 1, type: 'file', diff --git a/miniprogram/pages/history/index.js b/miniprogram/pages/history/index.js index fb8e497..6f0d362 100644 --- a/miniprogram/pages/history/index.js +++ b/miniprogram/pages/history/index.js @@ -1,5 +1,6 @@ // pages/history/index.js - 测试历史记录(一次拉取全部,时间行右侧展示企业名) const app = getApp() +const { getEffectiveEnterpriseId } = require('../../utils/enterpriseContext.js') Page({ data: { @@ -15,10 +16,7 @@ Page({ const gd = app.globalData || {} const storedUser = wx.getStorageSync('userInfo') || null const scope = gd.appScope || 'personal' - const enterpriseId = gd.enterpriseIdFromScene - || (gd.userInfo && gd.userInfo.enterpriseId) - || (storedUser && storedUser.enterpriseId) - || null + const enterpriseId = getEffectiveEnterpriseId() return scope === 'enterprise' || !!enterpriseId }, diff --git a/miniprogram/pages/index/result.js b/miniprogram/pages/index/result.js index c7d9feb..59d048c 100644 --- a/miniprogram/pages/index/result.js +++ b/miniprogram/pages/index/result.js @@ -2,6 +2,7 @@ const app = getApp() const payment = require('../../utils/payment') const { hasPhone, bindPhoneByCode, isProfileComplete } = require('../../utils/phoneAuth.js') +const { getEnterpriseIdForApiPayload } = require('../../utils/enterpriseContext.js') Page({ data: { @@ -182,12 +183,8 @@ Page({ this.setData({ progress: Math.floor(progress), analyzingTip: tips[tipIndex] }) }, 200) - // 调用后端API:appScope='enterprise' 时才传 enterpriseId,个人版不传 - const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {} - const scope = (app.globalData && app.globalData.appScope) || 'personal' - const enterpriseId = scope === 'enterprise' - ? (app.globalData.enterpriseIdFromScene || userInfo.enterpriseId || null) - : null + // 个人版入口不带企业参数;企业版才回落绑定/默认企业 + const enterpriseId = getEnterpriseIdForApiPayload() wx.request({ url: `${app.globalData.apiBase}/api/analyze`, method: 'POST', @@ -198,7 +195,7 @@ Page({ data: { photoUrls: photos, userId: app.globalData.openId || '', - ...(enterpriseId ? { enterpriseId: Number(enterpriseId) } : {}) + ...(enterpriseId != null ? { enterpriseId: Number(enterpriseId) } : {}) }, success: (res) => { clearInterval(timer) diff --git a/miniprogram/pages/index/result.wxml b/miniprogram/pages/index/result.wxml index 88c26a5..0908faf 100644 --- a/miniprogram/pages/index/result.wxml +++ b/miniprogram/pages/index/result.wxml @@ -53,98 +53,108 @@ - - - 性格类型分析 + + + + 性格类型分析 - - - {{result.mbti}} - {{result.title}} - {{result.summary}} - - - - - - PDP主性格 - {{result.pdp || '--'}} - 辅助: {{result.pdpAux}} + + {{result.mbti}} + {{result.title}} + {{result.summary}} - - DISC类型 - {{result.disc || '--'}}型 - 辅助: {{result.discAux}} + + + + PDP主性格 + {{result.pdp || '--'}} + 辅助: {{result.pdpAux}} + + + DISC类型 + {{result.disc || '--'}}型 + 辅助: {{result.discAux}} + + + + + 主要优势 + + + + {{item}} + + + + + + 下一步:详细性格测试 → - - - 主要优势 - - - - {{item}} + + 性格概述 + {{result.summary}} + + + + 盖洛普前三大优势 + + + {{index + 1}} + {{item}} - - 下一步:详细性格测试 → - - - - - - 性格概述 - {{result.summary}} - - - - - 盖洛普前三大优势 - - - {{index + 1}} - {{item}} - - - - - - - - 完整版性格深度解析 - 解锁后将展示完整报告内容 - - - 完善资料 - - - - - - 解锁完整报告 - ¥{{payInfo.amountYuan || 0}} / 次 + + + + + + + 完整版性格深度解析 + 解锁后将展示完整报告内容 + + 完善资料 + + + + + 解锁完整报告 + ¥{{payInfo.amountYuan || 0}} / 次 + + + 一次性解锁本次{{reportTitle || '分析报告'}},永久保存在「历史记录」中 - - 一次性解锁本次{{reportTitle || '分析报告'}},永久保存在「历史记录」中 + diff --git a/miniprogram/pages/index/result.wxss b/miniprogram/pages/index/result.wxss index 2efea3f..0433031 100644 --- a/miniprogram/pages/index/result.wxss +++ b/miniprogram/pages/index/result.wxss @@ -273,6 +273,156 @@ text-align: center; } +.paywall-tip-panel { + margin-top: 16rpx; + padding: 0 8rpx; + line-height: 1.5; +} + +/* ========== 报告预览门禁(人脸分析报告):整区遮罩 + 预览截断 ========== */ +.report-gate-wrap { + position: relative; + margin-bottom: 24rpx; +} + +.report-gate-overlay { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 20; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: stretch; + overflow: hidden; +} + +/* 占据遮罩下半区,组内标题/说明/按钮垂直居中,避免贴底留白过大 */ +.report-gate-cta-wrap { + width: 100%; + box-sizing: border-box; + /* 占遮罩层约一半高度,保证内部 flex 垂直居中真正留白上下对称 */ + height: 48%; + min-height: 400rpx; + max-height: 640rpx; + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; + /* 整体上移,避免贴底 */ + margin-bottom: 96rpx; + padding: 16rpx 0 calc(20rpx + env(safe-area-inset-bottom)); +} + +.report-gate-gradient { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + pointer-events: none; + /* 高透明白雾,避免大块灰底发闷 */ + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.22) 10%, + rgba(255, 255, 255, 0.48) 28%, + rgba(255, 255, 255, 0.72) 48%, + rgba(255, 255, 255, 0.88) 100% + ); +} + +.report-gate-panel { + position: relative; + z-index: 2; + width: 100%; + box-sizing: border-box; + padding: 20rpx 20rpx 8rpx; + flex-shrink: 0; + background: linear-gradient(180deg, transparent 0%, rgba(255, 255, 255, 0.5) 18%, rgba(255, 255, 255, 0.96) 55%); +} + +.report-gate-panel .paywall-fake-title { + text-align: center; +} + +.report-gate-panel .paywall-tip { + text-align: center; +} + +/* 遮罩内主按钮:不再用绝对定位贴底,避免区域过矮 */ +.paywall-btn-inline { + position: relative !important; + left: auto !important; + right: auto !important; + bottom: auto !important; + width: 90% !important; + max-width: 640rpx; + margin: 28rpx auto 0; + box-sizing: border-box; +} + +.report-gate-panel .paywall-btn-inline::after { + border: none; +} + +/* 未解锁:限制概述与盖洛普可见行数 */ +.report-gate-teaser--locked .card-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; + overflow: hidden; + text-overflow: ellipsis; +} + +.report-gate-teaser--locked .gallup-list { + max-height: 96rpx; + overflow: hidden; + position: relative; +} + +.report-gate-teaser--locked .gallup-list::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 48rpx; + background: linear-gradient(180deg, rgba(255, 255, 255, 0), #fff); +} + +/* 未解锁:压缩首屏性格区可读内容 */ +.personality-card--locked .type-desc { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + text-overflow: ellipsis; +} + +.personality-card--locked .advantages-box { + max-height: 140rpx; + overflow: hidden; + position: relative; +} + +.personality-card--locked .advantages-box::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 56rpx; + background: linear-gradient(180deg, rgba(255, 255, 255, 0), #fff); +} + +.personality-card--locked .next-btn { + opacity: 0.45; + pointer-events: none; +} + /* ========== 性格类型卡片 ========== */ .personality-card { text-align: center; diff --git a/miniprogram/pages/promo/poster.js b/miniprogram/pages/promo/poster.js index 1c6186e..f1ed8fa 100644 --- a/miniprogram/pages/promo/poster.js +++ b/miniprogram/pages/promo/poster.js @@ -1,4 +1,5 @@ const { getApiBase } = require('../../utils/request') +const { getEffectiveEnterpriseId } = require('../../utils/enterpriseContext.js') Page({ data: { @@ -21,9 +22,7 @@ Page({ const token = wx.getStorageSync('token') || gd.token || '' const scope = gd.appScope || 'personal' // 企业 ID:按优先级取 scene > globalData.userInfo > storage.userInfo - const eid = scope === 'enterprise' - ? (gd.enterpriseIdFromScene || (gd.userInfo && gd.userInfo.enterpriseId) || storedUser.enterpriseId || null) - : null + const eid = scope === 'enterprise' ? getEffectiveEnterpriseId() : null let url = apiBase.replace(/\/$/, '') + '/api/distribution/poster' if (eid) url += `?eid=${eid}&scope=enterprise` else if (scope === 'enterprise') url += '?scope=enterprise' // 企业模式但 eid 未知,后端从 DB 取 diff --git a/miniprogram/pages/recharge/index.js b/miniprogram/pages/recharge/index.js index c9e060f..d5af57d 100644 --- a/miniprogram/pages/recharge/index.js +++ b/miniprogram/pages/recharge/index.js @@ -1,6 +1,7 @@ const app = getApp() const payment = require('../../utils/payment') const { request } = require('../../utils/request') +const { getEffectiveEnterpriseId } = require('../../utils/enterpriseContext.js') Page({ data: { @@ -29,15 +30,31 @@ Page({ app.globalData.enterpriseIdFromScene = enterpriseId } + const resolveEnterpriseId = () => { + if (enterpriseId > 0) return enterpriseId + return getEffectiveEnterpriseId() || 0 + } + this.setData({ - enterpriseId, + enterpriseId: resolveEnterpriseId(), enterpriseName: (app.globalData.userInfo && app.globalData.userInfo.enterpriseName) || '', amountFen, amountYuan }) app.ensureLogin() + .then((ok) => { + if (!ok) { + wx.showToast({ title: '请先登录', icon: 'none' }) + return Promise.reject(new Error('no login')) + } + return app.getRuntimeConfig().catch(() => {}) + }) .then(() => { + const finalEid = resolveEnterpriseId() + if (finalEid > 0 && enterpriseId <= 0) { + this.setData({ enterpriseId: finalEid }) + } if (enterpriseId > 0) { request({ url: '/api/enterprise/bind', @@ -52,9 +69,7 @@ Page({ }) } }) - .catch(() => { - wx.showToast({ title: '请先登录', icon: 'none' }) - }) + .catch(() => {}) }, submitRecharge() { diff --git a/miniprogram/pages/result/disc.wxss b/miniprogram/pages/result/disc.wxss index 4c17f45..61eae0f 100644 --- a/miniprogram/pages/result/disc.wxss +++ b/miniprogram/pages/result/disc.wxss @@ -187,11 +187,11 @@ } .paywall-card { margin-top: 24rpx; border-radius: 24rpx; overflow: hidden; } -.paywall-content { position: relative; } -.paywall-blur { padding: 32rpx 24rpx 140rpx; border-radius: 24rpx; background: rgba(255,255,255,0.7); backdrop-filter: blur(18rpx); } -.paywall-mask { position: absolute; left: 0; top: 0; right: 0; bottom: 0; border-radius: 24rpx; background: linear-gradient(180deg, rgba(255,255,255,0.1), rgba(245,245,245,0.98)); pointer-events: none; } +.paywall-content { position: relative; min-height: 360rpx; } +.paywall-blur { padding: 32rpx 24rpx 200rpx; border-radius: 24rpx; background: rgba(255,255,255,0.97); backdrop-filter: blur(6rpx); } +.paywall-mask { position: absolute; left: 0; top: 0; right: 0; bottom: 0; border-radius: 24rpx; z-index: 1; background: linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.28) 38%, rgba(255,255,255,0.58) 100%); pointer-events: none; } .paywall-fake-title { display: block; font-size: 30rpx; font-weight: 600; color: #444; margin-bottom: 16rpx; } .paywall-fake-line { display: block; font-size: 26rpx; color: #888; line-height: 1.8; } -.paywall-btn { position: absolute; left: 5%; right: 5%; width: 90%; bottom: 56rpx; 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 { 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; } diff --git a/miniprogram/pages/result/mbti.wxss b/miniprogram/pages/result/mbti.wxss index ccbf617..46c458c 100644 --- a/miniprogram/pages/result/mbti.wxss +++ b/miniprogram/pages/result/mbti.wxss @@ -205,27 +205,36 @@ border-radius: 24rpx; overflow: hidden; } -.paywall-content { position: relative; } +.paywall-content { position: relative; min-height: 360rpx; } .paywall-blur { - padding: 32rpx 24rpx 140rpx; + padding: 32rpx 24rpx 200rpx; border-radius: 24rpx; - background: rgba(255, 255, 255, 0.7); - backdrop-filter: blur(18rpx); + background: rgba(255, 255, 255, 0.97); + backdrop-filter: blur(6rpx); } .paywall-mask { position: absolute; left: 0; top: 0; right: 0; bottom: 0; border-radius: 24rpx; - background: linear-gradient(180deg, rgba(255,255,255,0.1), rgba(245,245,245,0.98)); + z-index: 1; + /* 更轻的白纱,底部也保持通透 */ + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.08) 0%, + rgba(255, 255, 255, 0.28) 38%, + rgba(255, 255, 255, 0.58) 100% + ); pointer-events: none; } .paywall-fake-title { display: block; font-size: 30rpx; font-weight: 600; color: #444; margin-bottom: 16rpx; } .paywall-fake-line { display: block; font-size: 26rpx; color: #888; line-height: 1.8; } .paywall-btn { position: absolute; - left: 5%; right: 5%; + left: 5%; + right: 5%; width: 90%; - bottom: 56rpx; + bottom: 132rpx; + z-index: 3; padding: 20rpx 0; border-radius: 999rpx; background: linear-gradient(135deg, #FF6B8A 0%, #ff8fa3 100%); diff --git a/miniprogram/pages/result/pdp.wxss b/miniprogram/pages/result/pdp.wxss index e70c699..c799dbd 100644 --- a/miniprogram/pages/result/pdp.wxss +++ b/miniprogram/pages/result/pdp.wxss @@ -201,11 +201,11 @@ } .paywall-card { margin-top: 24rpx; border-radius: 24rpx; overflow: hidden; } -.paywall-content { position: relative; } -.paywall-blur { padding: 32rpx 24rpx 140rpx; border-radius: 24rpx; background: rgba(255,255,255,0.7); backdrop-filter: blur(18rpx); } -.paywall-mask { position: absolute; left: 0; top: 0; right: 0; bottom: 0; border-radius: 24rpx; background: linear-gradient(180deg, rgba(255,255,255,0.1), rgba(245,245,245,0.98)); pointer-events: none; } +.paywall-content { position: relative; min-height: 360rpx; } +.paywall-blur { padding: 32rpx 24rpx 200rpx; border-radius: 24rpx; background: rgba(255,255,255,0.97); backdrop-filter: blur(6rpx); } +.paywall-mask { position: absolute; left: 0; top: 0; right: 0; bottom: 0; border-radius: 24rpx; z-index: 1; background: linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.28) 38%, rgba(255,255,255,0.58) 100%); pointer-events: none; } .paywall-fake-title { display: block; font-size: 30rpx; font-weight: 600; color: #444; margin-bottom: 16rpx; } .paywall-fake-line { display: block; font-size: 26rpx; color: #888; line-height: 1.8; } -.paywall-btn { position: absolute; left: 5%; right: 5%; width: 90%; bottom: 56rpx; 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 { 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; } diff --git a/miniprogram/pages/result/resume.js b/miniprogram/pages/result/resume.js index bb18907..818422d 100644 --- a/miniprogram/pages/result/resume.js +++ b/miniprogram/pages/result/resume.js @@ -1,6 +1,7 @@ // pages/result/resume.js - 简历综合分析结果页 const app = getApp() const payment = require('../../utils/payment') +const { getEnterpriseIdForApiPayload } = require('../../utils/enterpriseContext.js') Page({ data: { @@ -111,13 +112,7 @@ Page({ const apiBase = (app.globalData && app.globalData.apiBase) || wx.getStorageSync('apiBase') || '' const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || '' - // 获取 enterpriseId - const gd = app.globalData || {} - const storedUser = wx.getStorageSync('userInfo') || null - const enterpriseId = gd.enterpriseIdFromScene - || (gd.userInfo && gd.userInfo.enterpriseId) - || (storedUser && storedUser.enterpriseId) - || null + const enterpriseId = getEnterpriseIdForApiPayload() const postData = {} if (this.data.fileUrl) postData.fileUrl = this.data.fileUrl diff --git a/miniprogram/utils/enterpriseContext.js b/miniprogram/utils/enterpriseContext.js new file mode 100644 index 0000000..f7ee639 --- /dev/null +++ b/miniprogram/utils/enterpriseContext.js @@ -0,0 +1,52 @@ +/** + * 小程序企业上下文:显式入口参数 > 用户绑定企业 > 超管配置的默认企业 + */ + +/** + * 企业版落地页、分享带 eid、充值等:完整回落链 + * @returns {number|null} + */ +function getEffectiveEnterpriseId() { + const app = getApp() + const gd = app.globalData || {} + const fromScene = gd.enterpriseIdFromScene + if (fromScene != null && Number(fromScene) > 0) { + return Number(fromScene) + } + const u = gd.userInfo || wx.getStorageSync('userInfo') || {} + const bound = u.enterpriseId + if (bound != null && Number(bound) > 0) { + return Number(bound) + } + const def = gd.defaultEnterpriseId + if (def != null && Number(def) > 0) { + return Number(def) + } + return null +} + +/** + * 提交给后端的 enterpriseId(analyze、test/submit、支付创建订单、简历分析等) + * - 个人版 Tab(appScope=personal):仅传递「本次入口携带的 eid」(enterpriseIdFromScene), + * 不因「已绑定企业」或「超管默认企业」带参,避免个人入口却走企业定价/落库企业字段。 + * - 企业版(appScope=enterprise):与 getEffectiveEnterpriseId 一致。 + * @returns {number|null} + */ +function getEnterpriseIdForApiPayload() { + const app = getApp() + const gd = app.globalData || {} + const scope = gd.appScope || 'personal' + if (scope === 'personal') { + const fromScene = gd.enterpriseIdFromScene + if (fromScene != null && Number(fromScene) > 0) { + return Number(fromScene) + } + return null + } + return getEffectiveEnterpriseId() +} + +module.exports = { + getEffectiveEnterpriseId, + getEnterpriseIdForApiPayload +} diff --git a/miniprogram/utils/payment.js b/miniprogram/utils/payment.js index b29971b..d965b88 100644 --- a/miniprogram/utils/payment.js +++ b/miniprogram/utils/payment.js @@ -2,6 +2,13 @@ // 微信支付工具类 - 复刻自Soul项目 const app = getApp() +const { getEnterpriseIdForApiPayload } = require('./enterpriseContext.js') + +/** 创建订单时传给后端的 enterpriseId(0 表示无企业上下文);与个人/企业 Tab 一致 */ +function enterpriseIdForOrder() { + const eid = getEnterpriseIdForApiPayload() + return eid != null && Number(eid) > 0 ? Number(eid) : 0 +} /** * 生成符合微信规则的订单号 @@ -364,6 +371,7 @@ function purchaseByPricing(productType, description, extra, maybeFail) { description, productType, testResultId, + enterpriseId: enterpriseIdForOrder(), success, fail }) @@ -486,6 +494,7 @@ function purchasePersonalDeepService(arg1, arg2, arg3) { description: desc, productType: 'deep_personal', deepProductId, + enterpriseId: enterpriseIdForOrder(), success, fail }) @@ -502,6 +511,7 @@ function purchaseTeamDeepService(success, fail) { amount: 0, description: '团队深度服务(团队画像+策略)', productType: 'deep_team', + enterpriseId: enterpriseIdForOrder(), success, fail }) diff --git a/miniprogram/utils/share.js b/miniprogram/utils/share.js index 0be4071..86ebee2 100644 --- a/miniprogram/utils/share.js +++ b/miniprogram/utils/share.js @@ -8,6 +8,8 @@ * 接收方(落地页 onLoad options)直接读 options.uid / options.eid */ +const { getEffectiveEnterpriseId } = require('./enterpriseContext.js') + function getApp_() { try { return getApp() } catch (e) { return null } } @@ -24,9 +26,7 @@ function buildShareQuery() { const parts = [] if (uid) parts.push('uid=' + uid) if (scope === 'enterprise') { - const eid = (app && app.globalData && app.globalData.enterpriseIdFromScene) - || userInfo.enterpriseId - || '' + const eid = getEffectiveEnterpriseId() || '' if (eid) parts.push('eid=' + eid) } return parts.join('&')