feat: 双端题库统一与 PDP/DISC 结果文案、测评 API 与企测上下文

1、修复了测评/结果页与个人中心展示不一致问题;废弃独立 questions.js,统一使用 questionBank;结果格式与详情解析相关问题。

2、新增了 PdpDiscResultText(PDP+DISC 双类型摘要文案)、抖音 questionBank 与 enterpriseContext;扩展 api/Test 与路由、管理端 Order 与 ExtractsTestResults。

3、优化了 resultFormat 与 questionBank 组织方式、UserDetailDialog;移除不再使用的 mbti_test_results 恢复 SQL/脚本。

Made-with: Cursor
This commit is contained in:
Ghost
2026-03-31 15:26:21 +08:00
parent 3fdf9b6ed2
commit e672ad4ede
48 changed files with 1455 additions and 627 deletions

View File

@@ -0,0 +1,250 @@
<?php
namespace app\common;
/**
* PDP / DISC 结果摘要:权重最高的 2 项。
* 展示格式DISC 为「D+I型」仅最后一项带「型」PDP 为「孔雀+老虎型」。
*/
class PdpDiscResultText
{
private const PDP_EN_TO_CN = [
'Tiger' => '老虎型',
'Peacock' => '孔雀型',
'Koala' => '考拉型',
'Owl' => '猫头鹰型',
'Chameleon' => '变色龙型',
];
/**
* @param array<string,mixed> $data
*/
public static function discTopTwo(array $data): string
{
[$fL, $sL] = self::discResolveTwoLetters($data);
if ($fL !== '' || $sL !== '') {
if ($fL === '') {
return $sL !== '' ? ($sL . '型') : '';
}
if ($sL === '' || $sL === $fL) {
return $fL . '型';
}
return $fL . '+' . $sL . '型';
}
return self::discNormalizeLegacyDualDescription($data['description']['type'] ?? null) ?? '';
}
/**
* @param array<string,mixed> $data
*/
public static function pdpTopTwo(array $data): string
{
[$firstFull, $secondFull] = self::pdpResolveTwoFull($data);
if ($firstFull === '') {
return $secondFull;
}
if ($secondFull === '' || $secondFull === $firstFull) {
return $firstFull;
}
$firstShort = preg_replace('/型$/u', '', $firstFull);
return $firstShort . '+' . $secondFull;
}
/**
* @return array{0:string,1:string} DISC 字母 D/I/S/C
*/
private static function discResolveTwoLetters(array $data): array
{
$f = self::discPrimaryLetter($data);
$s = '';
$sk = $data['secondaryType'] ?? null;
if (is_string($sk) && $sk !== '') {
$u = strtoupper(substr(trim($sk), 0, 1));
if (in_array($u, ['D', 'I', 'S', 'C'], true)) {
$s = $u;
}
}
$a = '';
$b = '';
if (isset($data['scores']) && is_array($data['scores'])) {
[$a, $b] = self::discOrderedLetters($data['scores']);
}
if ($a === '' && $b === '' && isset($data['percentages']) && is_array($data['percentages'])) {
[$a, $b] = self::discOrderedLetters($data['percentages']);
}
if ($f === '' && $a !== '') {
$f = $a;
}
if ($s === '' || $s === $f) {
if ($b !== '' && $b !== $f) {
$s = $b;
} else {
$s = '';
}
}
return [$f, $s];
}
/**
* 旧数据「S型 + I型」等 → 「S+I型」
*/
private static function discNormalizeLegacyDualDescription(?string $desc): ?string
{
if ($desc === null || trim($desc) === '') {
return null;
}
$t = preg_replace('/\s+/u', '', trim($desc));
$t = str_replace('', '+', $t);
if (preg_match('/^([DISC])型\+([DISC])型$/iu', $t, $m)) {
return strtoupper($m[1]) . '+' . strtoupper($m[2]) . '型';
}
if (preg_match('/^([DISC])型$/iu', $t, $m)) {
return strtoupper($m[1]) . '型';
}
return null;
}
/**
* @return array{0:string,1:string} 完整 PDP 类型文案(含「型」)
*/
private static function pdpResolveTwoFull(array $data): array
{
$f = self::pdpPrimaryFull($data);
$s = '';
$sk = $data['secondaryType'] ?? null;
if (is_string($sk) && $sk !== '') {
$s = self::PDP_EN_TO_CN[$sk] ?? $sk;
}
$aEn = '';
$bEn = '';
if (isset($data['scores']) && is_array($data['scores'])) {
[$aEn, $bEn] = self::pdpOrderedKeys($data['scores']);
}
if ($aEn === '' && $bEn === '' && isset($data['percentages']) && is_array($data['percentages'])) {
[$aEn, $bEn] = self::pdpOrderedKeys($data['percentages']);
}
if ($f === '' && $aEn !== '') {
$f = self::PDP_EN_TO_CN[$aEn] ?? $aEn;
}
if ($s === '' || $s === $f) {
if ($bEn !== '') {
$cand = self::PDP_EN_TO_CN[$bEn] ?? $bEn;
if ($cand !== $f) {
$s = $cand;
}
}
}
return [$f, $s];
}
private static function discPrimaryLetter(array $data): string
{
$desc = $data['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
$t = trim($desc);
$noXing = preg_replace('/型$/u', '', $t);
if (mb_strlen($noXing) === 1) {
$u = strtoupper($noXing);
if (in_array($u, ['D', 'I', 'S', 'C'], true)) {
return $u;
}
}
}
$dom = $data['dominantType'] ?? null;
if (is_string($dom) && $dom !== '') {
$u = strtoupper(substr(trim($dom), 0, 1));
if (in_array($u, ['D', 'I', 'S', 'C'], true)) {
return $u;
}
}
$disc = $data['disc'] ?? null;
if (is_string($disc) && $disc !== '') {
$t = trim($disc);
$noXing = preg_replace('/型$/u', '', $t);
if (mb_strlen($noXing) === 1) {
$u = strtoupper($noXing);
if (in_array($u, ['D', 'I', 'S', 'C'], true)) {
return $u;
}
}
}
return '';
}
private static function pdpPrimaryFull(array $data): string
{
$desc = $data['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return trim($desc);
}
$dom = $data['dominantType'] ?? null;
if (is_string($dom) && $dom !== '') {
return self::PDP_EN_TO_CN[$dom] ?? trim($dom);
}
$pdp = $data['pdp'] ?? null;
if (is_string($pdp) && $pdp !== '') {
return trim($pdp);
}
return '';
}
/**
* @param array<string,int|float> $scores
* @return array{0:string,1:string}
*/
private static function discOrderedLetters(array $scores): array
{
$allowed = ['D' => true, 'I' => true, 'S' => true, 'C' => true];
$pairs = [];
foreach ($scores as $k => $v) {
if (!is_string($k) && !is_numeric($k)) {
continue;
}
$ku = strtoupper(substr(trim((string) $k), 0, 1));
if (!isset($allowed[$ku])) {
continue;
}
$pairs[] = [$ku, (int) $v];
}
usort($pairs, static function ($a, $b) {
return ($b[1] <=> $a[1]) ?: strcmp($a[0], $b[0]);
});
$f = $pairs[0][0] ?? '';
$s = $pairs[1][0] ?? '';
return [$f, $s];
}
/**
* @param array<string,int|float> $scores
* @return array{0:string,1:string}
*/
private static function pdpOrderedKeys(array $scores): array
{
$pairs = [];
foreach ($scores as $k => $v) {
if (!is_string($k)) {
continue;
}
$key = trim($k);
if ($key === '' || !isset(self::PDP_EN_TO_CN[$key])) {
continue;
}
$pairs[] = [$key, (int) $v];
}
usort($pairs, static function ($a, $b) {
return ($b[1] <=> $a[1]) ?: strcmp($a[0], $b[0]);
});
$f = $pairs[0][0] ?? '';
$s = $pairs[1][0] ?? '';
return [$f, $s];
}
}