From 48c4d551d255f57381538ffb9072d28b1e424240 Mon Sep 17 00:00:00 2001 From: Ghost <106998207@qq.com> Date: Thu, 2 Apr 2026 09:40:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=81=E4=B8=9A=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=AB=AF=E8=AE=BE=E7=BD=AE=E9=A1=B5=E7=B2=BE=E7=AE=80?= =?UTF-8?q?=E4=B8=8E=20CRM=20=E6=8A=A5=E5=91=8A=E6=8E=A5=E5=8F=A3=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、修复了管理端系统设置交互与表单项冗余问题。 2、优化了 admin Settings 控制器与新 Settings 页面对齐。 3、调整 CrmReport 接口逻辑与数据结构。 Made-with: Cursor --- admin/src/views/admin/Settings.vue | 180 ++++++++++---------------- api/app/controller/admin/Settings.php | 64 ++++++--- api/app/controller/api/CrmReport.php | 108 ++++++++++------ 3 files changed, 191 insertions(+), 161 deletions(-) diff --git a/admin/src/views/admin/Settings.vue b/admin/src/views/admin/Settings.vue index ab2ef3c..b753510 100644 --- a/admin/src/views/admin/Settings.vue +++ b/admin/src/views/admin/Settings.vue @@ -44,61 +44,42 @@ - - - -
-
-

存客宝 Key

-

- 按测评类型配置企业版与个人版 Key(人脸、MBTI、DISC、PDP 各自独立),数据仅保存在当前企业维度;留空表示未配置。下方「上报时机」对上述四类均生效:免费测评完成后即上报;若该类型标价需付费,可选付款后才上报或测试完即上报。 -

-
-
-
-
{{ row.label }}
-
-
- +
@@ -170,7 +151,7 @@ import { request } from '@/utils/request' import { getAdminRole } from '@/utils/authStorage' import Finance from './Finance.vue' -const TAB_IDS = ['account', 'features', 'cunkebao', 'finance'] as const +const TAB_IDS = ['account', 'features', 'finance'] as const type TabId = (typeof TAB_IDS)[number] function isTabId(s: string): s is TabId { @@ -192,22 +173,21 @@ const canConfigureCunkebaoKeys = () => { const tabs = computed(() => { const rows: { label: string; value: TabId }[] = [{ label: '账号设置', value: 'account' }] - if (isEnterpriseAdmin()) { + if (isEnterpriseAdmin() || canConfigureCunkebaoKeys()) { rows.push({ label: '功能开关', value: 'features' }) } - if (canConfigureCunkebaoKeys()) { - rows.push({ label: '存客宝key', value: 'cunkebao' }) - } rows.push({ label: '企业余额', value: 'finance' }) return rows }) const applyRouteTab = () => { const t = route.query.tab + if (typeof t === 'string' && t === 'cunkebao') { + activeTab.value = 'features' + return + } if (typeof t === 'string' && isTabId(t)) { - if (t === 'features' && !isEnterpriseAdmin()) { - activeTab.value = 'account' - } else if (t === 'cunkebao' && !canConfigureCunkebaoKeys()) { + if (t === 'features' && !isEnterpriseAdmin() && !canConfigureCunkebaoKeys()) { activeTab.value = 'account' } else { activeTab.value = t @@ -254,20 +234,11 @@ const permSaving = ref(false) const adminPermsCeiling = ref>(defaultAdminPermissions()) const adminPerms = reactive>(defaultAdminPermissions()) -type CunkebaoKeyScope = 'face' | 'pdp' | 'disc' | 'mbti' - -const cunkebaoRows: { key: CunkebaoKeyScope; label: string }[] = [ - { key: 'face', label: '人脸测试' }, - { key: 'pdp', label: 'PDP' }, - { key: 'disc', label: 'DISC' }, - { key: 'mbti', label: 'MBTI' } -] - -const cunkebaoKeys = reactive>({ - face: { enterprise: '', personal: '', reportTiming: 'after_paid' }, - pdp: { enterprise: '', personal: '', reportTiming: 'after_paid' }, - disc: { enterprise: '', personal: '', reportTiming: 'after_paid' }, - mbti: { enterprise: '', personal: '', reportTiming: 'after_paid' } +/** 测评类存客宝:共用一对 Key + 统一上报时机 */ +const cunkebaoUnified = reactive({ + enterprise: '', + personal: '', + reportTiming: 'after_paid' as 'after_paid' | 'after_test' }) const cunkebaoLoading = ref(false) @@ -280,14 +251,8 @@ const loadCunkebaoKeys = async () => { const res: any = await request.get('/admin/settings/cunkebao-keys') if (res.code === 200 && res.data?.cunkebaoKeys && typeof res.data.cunkebaoKeys === 'object') { const ck = res.data.cunkebaoKeys - cunkebaoRows.forEach(({ key }) => { - const row = ck[key] - if (row && typeof row === 'object') { - cunkebaoKeys[key].enterprise = String(row.enterprise ?? '') - cunkebaoKeys[key].personal = String(row.personal ?? '') - cunkebaoKeys[key].reportTiming = row.reportTiming === 'after_test' ? 'after_test' : 'after_paid' - } - }) + cunkebaoUnified.apiKey = String(ck.apiKey ?? '') + cunkebaoUnified.reportTiming = ck.reportTiming === 'after_test' ? 'after_test' : 'after_paid' } } catch (e: any) { console.error(e) @@ -302,7 +267,7 @@ const saveCunkebaoKeys = async () => { cunkebaoSaving.value = true try { const res: any = await request.put('/admin/settings/cunkebao-keys', { - cunkebaoKeys: { ...cunkebaoKeys } + cunkebaoKeys: { ...cunkebaoUnified } }) if (res.code === 200) { ElMessage.success('存客宝 Key 已保存') @@ -412,11 +377,13 @@ watch( watch( () => activeTab.value, (tab) => { - if (tab === 'features' && isEnterpriseAdmin()) { - loadAdminPermissions() - } - if (tab === 'cunkebao' && canConfigureCunkebaoKeys()) { - loadCunkebaoKeys() + if (tab === 'features') { + if (isEnterpriseAdmin()) { + loadAdminPermissions() + } + if (canConfigureCunkebaoKeys()) { + loadCunkebaoKeys() + } } } ) @@ -424,11 +391,13 @@ watch( onMounted(() => { applyRouteTab() loadSettings() - if (activeTab.value === 'features' && isEnterpriseAdmin()) { - loadAdminPermissions() - } - if (activeTab.value === 'cunkebao' && canConfigureCunkebaoKeys()) { - loadCunkebaoKeys() + if (activeTab.value === 'features') { + if (isEnterpriseAdmin()) { + loadAdminPermissions() + } + if (canConfigureCunkebaoKeys()) { + loadCunkebaoKeys() + } } }) @@ -464,28 +433,25 @@ onMounted(() => { margin: 0; } -.cunkebao-type-block { - margin-bottom: 28px; - padding-bottom: 20px; - border-bottom: 1px solid #f3f4f6; - - &:last-of-type { - border-bottom: none; - margin-bottom: 8px; - } +.cunkebao-embed-section { + margin-top: 28px; + padding-top: 24px; + border-top: 1px solid #e5e7eb; } -.cunkebao-section-title { - font-size: 15px; +.cunkebao-embed-title { + margin: 0 0 8px 0; + font-size: 16px; font-weight: 600; color: #111827; - margin-bottom: 14px; } -.cunkebao-two-col { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; +.cunkebao-embed-desc { + margin: 0 0 16px 0; +} + +.cunkebao-key-row { + max-width: 560px; } .cunkebao-timing-row { @@ -501,12 +467,6 @@ onMounted(() => { } } -@media (max-width: 900px) { - .cunkebao-two-col { - grid-template-columns: 1fr; - } -} - .perm-admin-row { display: flex; align-items: center; diff --git a/api/app/controller/admin/Settings.php b/api/app/controller/admin/Settings.php index e3ab607..d8955f5 100644 --- a/api/app/controller/admin/Settings.php +++ b/api/app/controller/admin/Settings.php @@ -175,22 +175,18 @@ class Settings extends BaseController } /** - * 存客宝 Key 默认结构(人脸/MBTI/PDP/DISC × 企业版与个人版) + * 存客宝默认结构(测评线索共用一个 Key + 上报时机) */ private static function defaultCunkebaoKeysStructure(): array { - $blank = ['enterprise' => '', 'personal' => '', 'reportTiming' => 'after_paid']; - return [ - 'face' => $blank, - 'pdp' => $blank, - 'disc' => $blank, - 'mbti' => $blank, + 'apiKey' => '', + 'reportTiming' => 'after_paid', ]; } /** - * @param mixed $raw 已解码的配置或 null + * @param mixed $raw 已解码的配置或 null(单 Key;兼容旧版 enterprise/personal 及按题型分栏) */ private static function normalizeCunkebaoKeysPayload($raw): array { @@ -198,16 +194,54 @@ class Settings extends BaseController if (!is_array($raw)) { return $out; } - foreach (array_keys($out) as $type) { + + $rtIn = static function ($v) use (&$out): void { + $rt = (string) ($v ?? ''); + if (in_array($rt, ['after_paid', 'after_test'], true)) { + $out['reportTiming'] = $rt; + } + }; + + // 新版:apiKey + reportTiming + if (array_key_exists('apiKey', $raw)) { + $out['apiKey'] = isset($raw['apiKey']) ? trim((string) $raw['apiKey']) : ''; + $rtIn($raw['reportTiming'] ?? ''); + + return $out; + } + + // 上一版:enterprise / personal 合二为一(优先企业栏) + if (array_key_exists('enterprise', $raw) || array_key_exists('personal', $raw)) { + $e = isset($raw['enterprise']) ? trim((string) $raw['enterprise']) : ''; + $p = isset($raw['personal']) ? trim((string) $raw['personal']) : ''; + $out['apiKey'] = $e !== '' ? $e : $p; + $rtIn($raw['reportTiming'] ?? ''); + + return $out; + } + + // 旧版:按题型分栏 + $types = ['face', 'pdp', 'disc', 'mbti']; + foreach ($types as $type) { if (!isset($raw[$type]) || !is_array($raw[$type])) { continue; } $row = $raw[$type]; - $out[$type]['enterprise'] = isset($row['enterprise']) ? trim((string) $row['enterprise']) : ''; - $out[$type]['personal'] = isset($row['personal']) ? trim((string) $row['personal']) : ''; - $out[$type]['reportTiming'] = in_array($row['reportTiming'] ?? '', ['after_paid', 'after_test'], true) - ? $row['reportTiming'] - : 'after_paid'; + if ($out['apiKey'] === '' && isset($row['enterprise'])) { + $x = trim((string) $row['enterprise']); + if ($x !== '') { + $out['apiKey'] = $x; + } + } + if ($out['apiKey'] === '' && isset($row['personal'])) { + $x = trim((string) $row['personal']); + if ($x !== '') { + $out['apiKey'] = $x; + } + } + if ((string) ($row['reportTiming'] ?? '') === 'after_test') { + $out['reportTiming'] = 'after_test'; + } } return $out; @@ -275,7 +309,7 @@ class Settings extends BaseController $sanitized = self::normalizeCunkebaoKeysPayload($payload); try { - self::saveConfig('cunkebao_keys', $sanitized, $eid, '存客宝 Key(本企业 · 人脸/MBTI/PDP/DISC)'); + self::saveConfig('cunkebao_keys', $sanitized, $eid, '存客宝 Key(本企业 · 测评类共用)'); return success($sanitized, '存客宝 Key 已保存'); } catch (\Exception $e) { diff --git a/api/app/controller/api/CrmReport.php b/api/app/controller/api/CrmReport.php index 00a069a..7db86c3 100644 --- a/api/app/controller/api/CrmReport.php +++ b/api/app/controller/api/CrmReport.php @@ -139,17 +139,15 @@ class CrmReport extends BaseController return null; } - $testScope = ''; $trEnterpriseId = 0; if ($testResultId > 0) { $tr = Db::name('test_results') ->where('id', $testResultId) ->where('userId', $userId) - ->field('enterpriseId,testScope') + ->field('enterpriseId') ->find(); if ($tr) { - $testScope = (string) ($tr['testScope'] ?? 'personal'); $trEnterpriseId = (int) ($tr['enterpriseId'] ?? 0); } } @@ -159,32 +157,15 @@ class CrmReport extends BaseController $configEid = (int) (Db::name('wechat_users')->where('id', $userId)->value('enterpriseId') ?? 0); } - $useEnterpriseColumn = ($testScope === 'enterprise') - || ($testScope === '' && $contextEnterpriseId > 0); - - $col = $useEnterpriseColumn ? 'enterprise' : 'personal'; - $key = self::readCunkebaoCellWithFallback($configEid, $testType, $col); + $key = self::readSingleCunkebaoApiKeyWithFallback($configEid); return $key !== '' ? $key : null; } /** - * 读取某企业 cunkebao_keys 中一格;为空时再读 enterprise_id=0 全局行 + * 读取企业 cunkebao_keys:单 apiKey;兼容旧版 enterprise/personal 及按题型分栏 */ - private static function readCunkebaoCellWithFallback(int $enterpriseId, string $testType, string $col): string - { - $v = self::readCunkebaoCell($enterpriseId, $testType, $col); - if ($v !== '') { - return $v; - } - if ($enterpriseId > 0) { - $v = self::readCunkebaoCell(0, $testType, $col); - } - - return $v; - } - - private static function readCunkebaoCell(int $enterpriseId, string $testType, string $col): string + private static function readSingleCunkebaoApiKey(int $enterpriseId): string { $row = Db::name('system_config') ->where('key', 'cunkebao_keys') @@ -194,11 +175,45 @@ class CrmReport extends BaseController return ''; } $decoded = is_string($row['value']) ? json_decode($row['value'], true) : $row['value']; - if (!is_array($decoded) || !isset($decoded[$testType][$col])) { + if (!is_array($decoded)) { return ''; } + if (array_key_exists('apiKey', $decoded)) { + return trim((string) ($decoded['apiKey'] ?? '')); + } + if (array_key_exists('enterprise', $decoded) || array_key_exists('personal', $decoded)) { + $e = trim((string) ($decoded['enterprise'] ?? '')); + $p = trim((string) ($decoded['personal'] ?? '')); - return trim((string) $decoded[$testType][$col]); + return $e !== '' ? $e : $p; + } + foreach (['face', 'mbti', 'pdp', 'disc'] as $t) { + if (!isset($decoded[$t]) || !is_array($decoded[$t])) { + continue; + } + $rowT = $decoded[$t]; + foreach (['enterprise', 'personal'] as $col) { + $v = trim((string) ($rowT[$col] ?? '')); + if ($v !== '') { + return $v; + } + } + } + + return ''; + } + + private static function readSingleCunkebaoApiKeyWithFallback(int $enterpriseId): string + { + $v = self::readSingleCunkebaoApiKey($enterpriseId); + if ($v !== '') { + return $v; + } + if ($enterpriseId > 0) { + $v = self::readSingleCunkebaoApiKey(0); + } + + return $v; } private static function testTypeSiteTag(string $testType): string @@ -352,13 +367,12 @@ class CrmReport extends BaseController // 未配置付费(免费):测试完成即上报(与「没付款可直接调用」一致) // 若需付费:仅当后台为「测试完即上报」时在提交后上报;「付款后才上报」则等支付成功后再走前端/接口 - $timing = self::readReportTiming($enterpriseId, $testType); + $timing = self::readReportTiming($enterpriseId); if ($requiresPayment === 1 && $timing !== 'after_test') { return; } - $col = ($testScope === 'enterprise') ? 'enterprise' : 'personal'; - $apiKey = self::readCunkebaoCellWithFallback($enterpriseId, $testType, $col); + $apiKey = self::readSingleCunkebaoApiKeyWithFallback($enterpriseId); if ($apiKey === '') { return; } @@ -382,22 +396,22 @@ class CrmReport extends BaseController } /** - * 读取某企业某测评类型的 reportTiming(after_paid / after_test) + * 读取企业 reportTiming(after_paid / after_test),测评类共用 */ - private static function readReportTiming(int $enterpriseId, string $testType): string + private static function readReportTiming(int $enterpriseId): string { - $val = self::readReportTimingFromRow($enterpriseId, $testType); + $val = self::readReportTimingFromRow($enterpriseId); if ($val !== '') { return $val; } if ($enterpriseId > 0) { - $val = self::readReportTimingFromRow(0, $testType); + $val = self::readReportTimingFromRow(0); } return $val !== '' ? $val : 'after_paid'; } - private static function readReportTimingFromRow(int $enterpriseId, string $testType): string + private static function readReportTimingFromRow(int $enterpriseId): string { $row = Db::name('system_config') ->where('key', 'cunkebao_keys') @@ -407,12 +421,34 @@ class CrmReport extends BaseController return ''; } $decoded = is_string($row['value']) ? json_decode($row['value'], true) : $row['value']; - if (!is_array($decoded) || !isset($decoded[$testType]['reportTiming'])) { + if (!is_array($decoded)) { return ''; } - $t = (string) $decoded[$testType]['reportTiming']; + if (isset($decoded['reportTiming'])) { + $t = (string) $decoded['reportTiming']; - return in_array($t, ['after_paid', 'after_test'], true) ? $t : ''; + return in_array($t, ['after_paid', 'after_test'], true) ? $t : ''; + } + foreach (['face', 'mbti', 'pdp', 'disc'] as $tKey) { + if (!isset($decoded[$tKey]['reportTiming'])) { + continue; + } + $t = (string) $decoded[$tKey]['reportTiming']; + if ($t === 'after_test') { + return 'after_test'; + } + } + foreach (['face', 'mbti', 'pdp', 'disc'] as $tKey) { + if (!isset($decoded[$tKey]['reportTiming'])) { + continue; + } + $t = (string) $decoded[$tKey]['reportTiming']; + if (in_array($t, ['after_paid', 'after_test'], true)) { + return $t; + } + } + + return ''; } /**