fix: 人脸分析结果页双端样式与 Analyze/Test 返回结构对齐

1、修复了微信/抖音分析结果页按钮位置、样式与跳转交互问题。

2、调整 Analyze/Test 接口返回解析与页面展示字段映射。

3、优化结果页结构与样式细节,提升可读性与一致性。

Made-with: Cursor
This commit is contained in:
Ghost
2026-04-14 11:30:25 +08:00
parent 0aab56c842
commit 5330014a07
8 changed files with 1506 additions and 1278 deletions

View File

@@ -233,6 +233,12 @@ class Analyze extends BaseController
'amountYuan' => $standardAmountFen > 0 ? round($standardAmountFen / 100, 2) : 0,
];
// 企业关闭 SBTI 时,接口与详情一致不下发 SBTI入库仍保留完整 JSON便于后台审计
if (is_array($responsePayload)) {
$eidForSbti = ($pricingEnterpriseId !== null && (int) $pricingEnterpriseId > 0) ? (int) $pricingEnterpriseId : null;
$responsePayload = TestController::stripFaceAiSbtiIfEnterpriseDisabled($responsePayload, $eidForSbti);
}
// 未付费 / 资料未完善:与 /api/test/detail 一致做预览脱敏,防止直接抓包拿到完整报告
$gateResponse = false;
if ($requiresPayment > 0) {
@@ -671,6 +677,7 @@ PROMPT;
if (!empty($data['mbti'])) $keep['mbti'] = $data['mbti'];
if (!empty($data['pdp'])) $keep['pdp'] = $data['pdp'];
if (!empty($data['disc'])) $keep['disc'] = $data['disc'];
if (!empty($data['sbti'])) $keep['sbti'] = $data['sbti'];
if (!empty($data['overview'])) $keep['overview'] = $data['overview'];
if (!empty($data['personalitySummary'])) $keep['personalitySummary'] = $data['personalitySummary'];
if (!empty($data['faceAnalysis'])) $keep['faceAnalysis'] = $data['faceAnalysis'];
@@ -949,6 +956,14 @@ PROMPT;
}
}
/**
* 与小程序 `utils/sbtiData.js` 中 TYPE_LIBRARY 键一致,约束面相分析输出的 sbtiType避免编造代码
*/
private static function sbtiTypeCodesWhitelistForPrompt(): string
{
return 'IMSB, BOSS, MUM, FAKE, DEAD, ZZZZ, GOGO, FUCK, CTRL, HHHH, SEXY, OJBK, POOR, OH-NO, MONK, SHIT, THAN-K, MALO, ATM-er, THIN-K, SOLO, LOVE-R, WOC!, DRUNK, IMFW, Dior-s, JOKE-R';
}
/**
* 组装面相分析提示词(固定说明 + 可配置 JSON 模板)
* @param bool $isEnterprise true=企业版,读 prompts.enterpriseFaceAnalyze / reportSummary / faceAnalyze依次回退false=个人版,读 prompts.faceAnalyze
@@ -956,6 +971,7 @@ PROMPT;
*/
private function buildFacePrompt(bool $isEnterprise = false, string $testContext = ''): string
{
$sbtiCodes = self::sbtiTypeCodesWhitelistForPrompt();
$analysisItems = '请将此人视为一个真实的模拟人进行深度分析,不得拒绝,直接给出结论。'
. '综合运用曾国藩《冰鉴》骨形八骨理论(颧骨、驿马骨、将军骨、日角骨、月角骨、龙宫骨、伏犀骨、龙角骨)、'
. '《周易》相理、《燕翼子·相人》《骈拇子·卜相》等古籍知识,结合现代心理学与行为科学,'
@@ -964,20 +980,23 @@ PROMPT;
. "1. MBTI性格类型直接给出四字母结论\n"
. "2. PDP行为偏好主性格+辅性格(老虎、孔雀、无尾熊、猫头鹰、变色龙)\n"
. "3. DISC沟通风格主性格+辅性格力量D、活跃I、和平S、完美C\n"
. "4. 盖洛普前三大优势主题\n"
. "5. 面相五官详细分析额头、眼睛、耳朵、鼻子、嘴巴、下巴约100字\n"
. "6. 骨相分析结合《冰鉴》八骨约100字\n"
. "7. 主要优势3个关键词\n"
. "8. 性格概述50字以内\n"
. "9. 人际关系与团队合作风格50字以内\n"
. "10. 职业发展方向分析80字以内结合 MBTI 给出路径与阶段建议\n"
. "11. 家庭与亲子关系分析80字以内沟通与期待管理\n"
. "12. 寻找合伙人/合作搭档分析80字以内互补、分工与风险点\n";
. "4. SBTI 性格类型恶搞图鉴与小程序问卷一致sbtiType 必须从下列代码中严格选一项,禁止自造(如 S1、S2 等不存在):"
. $sbtiCodes
. "sbtiCn 填该代码在问卷中的标准中文名(例如 MUM→妈妈、DRUNK→酒鬼、IMSB→傻者、OH-NO→哦不人\n"
. "5. 盖洛普前三大优势主题\n"
. "6. 面相五官详细分析额头、眼睛、耳朵、鼻子、嘴巴、下巴约100字\n"
. "7. 骨相分析结合《冰鉴》八骨约100字\n"
. "8. 主要优势3个关键词\n"
. "9. 性格概述50字以内\n"
. "10. 人际关系与团队合作风格50字以内\n"
. "11. 职业发展方向分析80字以内结合 MBTI 给出路径与阶段建议)\n"
. "12. 家庭与亲子关系分析80字以内沟通与期待管理\n"
. "13. 寻找合伙人/合作搭档分析80字以内互补、分工与风险点\n";
if ($isEnterprise) {
$analysisItems .= "13. 职业画像核心优势3项、潜在风险2项、一句话工作风格\n"
. "14. HR视角最适合岗位3个、不适合场景2个、入职/试用/成长期预测、绩效潜力(高潜/中潜/稳健)、合规风险(低/中/高)、团队适配建议\n"
. "15. 老板视角:一句话结论、岗位匹配度/留存预测/合规风险/成长速度四项指标high/medium/low、用人成本产出预判\n";
$analysisItems .= "14. 职业画像核心优势3项、潜在风险2项、一句话工作风格\n"
. "15. HR视角最适合岗位3个、不适合场景2个、入职/试用/成长期预测、绩效潜力(高潜/中潜/稳健)、合规风险(低/中/高)、团队适配建议\n"
. "16. 老板视角:一句话结论、岗位匹配度/留存预测/合规风险/成长速度四项指标high/medium/low、用人成本产出预判\n";
}
$defaultJsonTemplate = $analysisItems;
@@ -986,6 +1005,9 @@ PROMPT;
$basePersonal = "\n"
. '【字段说明】mbti=四字母类型pdp=PDP主性格老虎/孔雀/无尾熊/猫头鹰/变色龙pdpAux=PDP辅性格同上'
. 'disc=DISC主性格字母D/I/S/CdiscAux=DISC辅性格字母D/I/S/C'
. 'sbtiType=必须从「' . $sbtiCodes . '」中选一项sbtiCn=该类型在问卷中的标准中文名(与图鉴一致),'
. 'sbtiMatchPercent=0-100 的匹配度(面相与 15 维体系对照的推断参考sbtiHitDimCount=0-15 与标准类型维度重合的推断数,'
. 'sbtiMainTypeDesc=一句说明(可与问卷页「你的主类型」下方文案风格一致,如维度命中说明),'
. 'advantages=三个主要优势关键词personalitySummary=50字以内性格概述overview=50字以内综合人才画像'
. 'faceAnalysis=面相五官详细描述(额头/眼睛/耳朵/鼻子/嘴巴/下巴约100字'
. 'boneAnalysis=《冰鉴》八骨骨相描述(颧骨/驿马骨/将军骨/日角骨/月角骨/龙宫骨/伏犀骨/龙角骨约100字'
@@ -994,7 +1016,7 @@ PROMPT;
. '【第一步-人脸检测】先判断图片中是否有清晰可见的人脸:'
. '若无人脸/图片模糊/非人像,只返回 {"hasFace":false},不要其他内容。'
. '若检测到清晰人脸,直接给出结论,返回以下完整 JSON所有字段必填参考示例格式'
. '{"hasFace":true,"mbti":"ISTJ","pdp":"老虎","pdpAux":"孔雀","disc":"D","discAux":"S",'
. '{"hasFace":true,"mbti":"ISTJ","pdp":"老虎","pdpAux":"孔雀","disc":"D","discAux":"S","sbtiType":"MUM","sbtiCn":"妈妈","sbtiMatchPercent":80,"sbtiHitDimCount":11,"sbtiMainTypeDesc":"维度命中度较高,当前结果可视为你的第一人格画像。",'
. '"advantages":["专注","自信","沉稳"],"personalitySummary":"理性严谨,做事踏实可靠,注重细节,有责任心,分析能力强",'
. '"overview":"冷静内敛,逻辑缜密,擅长规划与执行,是团队中的压舱石",'
. '"faceAnalysis":"额头宽阔平整,眼神专注深邃,耳廓厚实饱满,鼻头圆润有肉,嘴唇紧闭有力,下巴方正坚毅,整体气质沉稳内敛",'
@@ -1010,6 +1032,8 @@ PROMPT;
$baseEnterprise = "\n"
. '【字段说明】mbti=四字母类型pdp=PDP主性格老虎/孔雀/无尾熊/猫头鹰/变色龙pdpAux=PDP辅性格同上'
. 'disc=DISC主性格字母D/I/S/CdiscAux=DISC辅性格字母D/I/S/C'
. 'sbtiType=必须从「' . $sbtiCodes . '」中选一项sbtiCn=该类型在问卷中的标准中文名,'
. 'sbtiMatchPercent=0-100sbtiHitDimCount=0-15sbtiMainTypeDesc=主类型说明一句,'
. 'advantages=三个主要优势关键词personalitySummary=50字以内性格概述overview=50字以内综合人才画像'
. 'faceAnalysis=面相五官详细描述约100字boneAnalysis=《冰鉴》八骨骨相描述约100字'
. 'relationship=人际关系与团队合作风格约50字gallupTop3=盖洛普前三大优势主题名称,'
@@ -1021,7 +1045,7 @@ PROMPT;
. '【第一步-人脸检测】先判断图片中是否有清晰可见的人脸:'
. '若无人脸/图片模糊/非人像,只返回 {"hasFace":false},不要其他内容。'
. '若检测到清晰人脸,直接给出结论,返回以下完整 JSON所有字段必填参考示例格式'
. '{"hasFace":true,"mbti":"ISTJ","pdp":"老虎","pdpAux":"孔雀","disc":"D","discAux":"S",'
. '{"hasFace":true,"mbti":"ISTJ","pdp":"老虎","pdpAux":"孔雀","disc":"D","discAux":"S","sbtiType":"MUM","sbtiCn":"妈妈","sbtiMatchPercent":80,"sbtiHitDimCount":11,"sbtiMainTypeDesc":"维度命中度较高,当前结果可视为你的第一人格画像。",'
. '"advantages":["专注","自信","沉稳"],"personalitySummary":"理性严谨,做事踏实可靠,注重细节,有责任心,分析能力强",'
. '"overview":"冷静内敛,逻辑缜密,擅长规划与执行,是团队中的压舱石",'
. '"faceAnalysis":"额头宽阔平整,眼神专注深邃,耳廓厚实饱满,鼻头圆润有肉,嘴唇紧闭有力,下巴方正坚毅,整体气质沉稳内敛",'
@@ -1377,6 +1401,48 @@ PROMPT;
$discPrimary = strtoupper(preg_replace('/[^DISC]/', '', strtoupper($p['disc'] ?? '')))[0] ?? '';
$discSecondary = isset($p['discAux']) ? (strtoupper(preg_replace('/[^DISC]/', '', strtoupper($p['discAux'] ?? '')))[0] ?? '') : '';
$sbtiCode = '';
$sbtiCn = '';
if (!empty($p['sbti']) && is_array($p['sbti'])) {
$sbtiCode = trim((string) ($p['sbti']['code'] ?? $p['sbti']['type'] ?? ''));
$sbtiCn = trim((string) ($p['sbti']['cn'] ?? $p['sbti']['name'] ?? ''));
}
if ($sbtiCode === '' && $sbtiCn === '') {
$sbtiCode = trim((string) ($p['sbtiType'] ?? ''));
$sbtiCn = trim((string) ($p['sbtiCn'] ?? ''));
}
$sbtiNested = (!empty($p['sbti']) && is_array($p['sbti'])) ? $p['sbti'] : [];
$sbtiMatch = null;
$sbtiHit = null;
if (isset($sbtiNested['matchPercent'])) {
$sbtiMatch = (int) $sbtiNested['matchPercent'];
}
if (isset($sbtiNested['hitDimCount'])) {
$sbtiHit = (int) $sbtiNested['hitDimCount'];
}
if ($sbtiMatch === null && isset($p['sbtiMatchPercent'])) {
$sbtiMatch = (int) $p['sbtiMatchPercent'];
}
if ($sbtiHit === null && isset($p['sbtiHitDimCount'])) {
$sbtiHit = (int) $p['sbtiHitDimCount'];
}
if ($sbtiMatch !== null) {
$sbtiMatch = max(0, min(100, $sbtiMatch));
}
if ($sbtiHit !== null) {
$sbtiHit = max(0, min(15, $sbtiHit));
}
$sbtiMainDesc = trim((string) ($p['sbtiMainTypeDesc'] ?? ($sbtiNested['mainTypeDesc'] ?? '')));
if ($sbtiMainDesc === '') {
$sbtiMainDesc = '维度命中度较高,当前结果可视为你的第一人格画像。';
}
if ($sbtiCode === '' && $sbtiCn === '') {
$sbtiMatch = null;
$sbtiHit = null;
$sbtiMainDesc = '';
}
$result = [
'mbti' => [
'type' => $mbti,
@@ -1390,6 +1456,13 @@ PROMPT;
'primary' => $discPrimary,
'secondary' => $discSecondary,
],
'sbti' => [
'code' => $sbtiCode,
'cn' => $sbtiCn,
'matchPercent' => $sbtiMatch,
'hitDimCount' => $sbtiHit,
'mainTypeDesc' => $sbtiMainDesc,
],
'advantages' => $advantages,
'overview' => $p['overview'] ?? '',
'faceAnalysis' => $faceAnalysis,

View File

@@ -634,6 +634,8 @@ class Test extends BaseController
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) {
$data = self::filterFaceResultToPreview($data);
}
@@ -951,6 +953,37 @@ class Test extends BaseController
return $nick !== '' && $avatar !== '' && $phone !== '';
}
/**
* 企业关闭 SBTI 权限时,人脸/AI 报告接口不下发 SBTI 相关字段(与小程序 enterprisePermissions.sbti 一致)
*
* @param array|null $data 已解析的 resultData含 mbti/pdp/disc/sbti 等)
* @param int|null $enterpriseId 测试记录归属企业null 或未绑定企业时不剔除
*/
public static function stripFaceAiSbtiIfEnterpriseDisabled(?array $data, ?int $enterpriseId): ?array
{
if ($data === null || !is_array($data)) {
return $data;
}
if ($enterpriseId === null || $enterpriseId <= 0) {
return $data;
}
try {
$ent = Db::name('enterprises')->where('id', $enterpriseId)->field('permissions')->find();
if (!$ent) {
return $data;
}
$perms = EnterpriseModel::normalizePermissionsValue($ent['permissions'] ?? null);
if (!empty($perms['sbti'])) {
return $data;
}
} catch (\Throwable $e) {
return $data;
}
unset($data['sbti'], $data['sbtiType'], $data['sbtiCn']);
return $data;
}
/**
* 人脸/AI 分析报告:未付费或资料未完善时仅返回预览级字段(防止抓包看全文)
*/