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

@@ -2,6 +2,7 @@
namespace app\controller\admin;
use app\BaseController;
use app\common\PdpDiscResultText;
use think\facade\Db;
use think\facade\Request;
@@ -156,6 +157,10 @@ class Order extends BaseController
return (string) ($data['mbtiType'] ?? $data['type'] ?? $data['result'] ?? '');
}
if ($type === 'disc') {
$two = PdpDiscResultText::discTopTwo($data);
if ($two !== '') {
return $two;
}
$desc = $data['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;
@@ -166,6 +171,10 @@ class Order extends BaseController
return (string) ($data['disc'] ?? '');
}
if ($type === 'pdp') {
$two = PdpDiscResultText::pdpTopTwo($data);
if ($two !== '') {
return $two;
}
$desc = $data['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;

View File

@@ -1,6 +1,8 @@
<?php
namespace app\controller\admin\concern;
use app\common\PdpDiscResultText;
/**
* 从测试记录数组中解析 MBTI / DISC / PDP / 人脸子类型(与 AppUser 逻辑一致)
*/
@@ -69,6 +71,10 @@ trait ExtractsTestResults
}
if ($targetType === 'disc') {
$two = PdpDiscResultText::discTopTwo($dec);
if ($two !== '') {
return $two;
}
$desc = $dec['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;
@@ -81,6 +87,10 @@ trait ExtractsTestResults
}
if ($targetType === 'pdp') {
$two = PdpDiscResultText::pdpTopTwo($dec);
if ($two !== '') {
return $two;
}
$desc = $dec['description']['type'] ?? null;
if (is_string($desc) && $desc !== '') {
return $desc;

View File

@@ -2,8 +2,10 @@
namespace app\controller\api;
use app\BaseController;
use app\common\PdpDiscResultText;
use app\model\Enterprise as EnterpriseModel;
use app\model\PricingConfig as PricingConfigModel;
use app\model\Question as QuestionModel;
use app\model\UserProfile as UserProfileModel;
use think\facade\Db;
use think\facade\Request;
@@ -144,20 +146,33 @@ class Test extends BaseController
], $paymentFields);
break;
case 'disc':
$discType = is_array($data) ? ($data['dominantType'] ?? $data['disc'] ?? '未知') : '未知';
$discTxt = '未知';
if (is_array($data)) {
$discTxt = PdpDiscResultText::discTopTwo($data);
if ($discTxt === '') {
$fallback = $data['dominantType'] ?? $data['disc'] ?? '未知';
$discTxt = (is_string($fallback) || is_numeric($fallback) ? (string) $fallback : '未知') . '型';
}
}
$list[] = array_merge([
'id' => $id,
'type' => 'disc',
'key' => 'disc_' . $id,
'emoji' => '📊',
'typeName' => 'DISC性格测试',
'resultText'=> (is_string($discType) || is_numeric($discType) ? (string) $discType : '未知') . '型',
'resultText'=> $discTxt,
'testTime' => $timeLabel,
'data' => null,
], $paymentFields);
break;
case 'pdp':
$primary = is_array($data) ? ($data['description']['type'] ?? $data['pdp'] ?? '未知') : '未知';
$primary = '未知';
if (is_array($data)) {
$primary = PdpDiscResultText::pdpTopTwo($data);
if ($primary === '') {
$primary = (string) ($data['description']['type'] ?? $data['pdp'] ?? '未知');
}
}
$emoji = (is_array($data) && isset($data['description']['emoji'])) ? $data['description']['emoji'] : '🦁';
$list[] = array_merge([
'id' => $id,
@@ -337,6 +352,36 @@ class Test extends BaseController
return array_values(array_unique($allowed));
}
/**
* 解析 test_results.resultData支持 JSON 字符串、已解码的 array、包在 result 键里的结构
*
* @param mixed $raw
* @return array<string,mixed>
*/
protected function decodeResultDataPayload($raw): array
{
if ($raw === null || $raw === '') {
return [];
}
if (is_array($raw)) {
$data = $raw;
} elseif (is_string($raw)) {
$decoded = json_decode(trim($raw), true);
$data = is_array($decoded) ? $decoded : [];
} else {
return [];
}
if (isset($data['result']) && is_array($data['result'])) {
$inner = $data['result'];
if (isset($inner['percentages']) || isset($inner['scores']) || isset($inner['dominantType'])
|| isset($inner['description']) || isset($inner['mbtiType'])) {
$data = array_merge($data, $inner);
}
}
return $data;
}
/**
* 格式化单条记录为 recent 接口返回结构
*/
@@ -345,11 +390,7 @@ class Test extends BaseController
$testType = $row['testType'] ?? '';
$createdAt = $row['createdAt'] ?? null;
$raw = $row['resultData'] ?? ($row['result'] ?? null);
$data = [];
if ($raw !== null && $raw !== '') {
$decoded = json_decode($raw, true);
$data = is_array($decoded) ? $decoded : [];
}
$data = $this->decodeResultDataPayload($raw);
$resultText = '';
$emoji = '';
@@ -363,13 +404,19 @@ class Test extends BaseController
$typeName = 'MBTI性格';
break;
case 'disc':
$dominantType = $data['dominantType'] ?? $data['disc'] ?? '未知';
$resultText = $dominantType . '';
$emoji = '📊';
$typeName = 'DISC测评';
$resultText = PdpDiscResultText::discTopTwo($data);
if ($resultText === '') {
$dominantType = $data['dominantType'] ?? $data['disc'] ?? '未知';
$resultText = (is_string($dominantType) || is_numeric($dominantType) ? (string) $dominantType : '未知') . '';
}
$emoji = '📊';
$typeName = 'DISC测评';
break;
case 'pdp':
$resultText = $data['description']['type'] ?? $data['pdp'] ?? '未知';
$resultText = PdpDiscResultText::pdpTopTwo($data);
if ($resultText === '') {
$resultText = $data['description']['type'] ?? $data['pdp'] ?? '未知';
}
$emoji = $data['description']['emoji'] ?? '🦁';
$typeName = 'PDP行为';
break;
@@ -397,6 +444,28 @@ class Test extends BaseController
}
}
// 小程序「最新测试」:始终附带 resultMeta便于前端 getTypeOnly避免仅 resultText 旧格式)
$resultMeta = null;
if ($testType === 'disc') {
$resultMeta = [
'scores' => $data['scores'] ?? null,
'percentages' => $data['percentages'] ?? null,
'dominantType' => $data['dominantType'] ?? null,
'secondaryType' => $data['secondaryType'] ?? null,
'description' => $data['description'] ?? null,
'disc' => $data['disc'] ?? null,
];
} elseif ($testType === 'pdp') {
$resultMeta = [
'scores' => $data['scores'] ?? null,
'percentages' => $data['percentages'] ?? null,
'dominantType' => $data['dominantType'] ?? null,
'secondaryType' => $data['secondaryType'] ?? null,
'description' => $data['description'] ?? null,
'pdp' => $data['pdp'] ?? null,
];
}
$out = [
'id' => (int) $row['id'],
'testType' => ($testType === 'face') ? 'ai' : $testType,
@@ -410,6 +479,9 @@ class Test extends BaseController
if ($gallupPreview !== '') {
$out['gallupPreview'] = $gallupPreview;
}
if ($resultMeta !== null) {
$out['resultMeta'] = $resultMeta;
}
return $out;
}
@@ -802,5 +874,90 @@ class Test extends BaseController
return $out;
}
/**
* 小程序拉取做题题库(仅启用题):企业本题库有题则用企业,否则用超管 enterpriseId 为空
* GET /api/test/questions?type=mbti|disc|pdp&enterpriseId=可选
*/
public function questions()
{
$user = $this->request->user ?? null;
if (!$user || ($user['source'] ?? '') !== 'wechat') {
return error('未登录', 401);
}
$type = (string) Request::param('type', '');
if (!in_array($type, ['mbti', 'disc', 'pdp'], true)) {
return error('type 须为 mbti、disc 或 pdp', 400);
}
$rawEid = Request::param('enterpriseId', null);
$enterpriseId = null;
if ($rawEid !== null && $rawEid !== '') {
$enterpriseId = (int) $rawEid;
if ($enterpriseId <= 0) {
$enterpriseId = null;
}
}
$resolvedEnterpriseId = null;
if ($enterpriseId !== null) {
$enterpriseQuestionCount = QuestionModel::where('enterpriseId', $enterpriseId)
->where('type', $type)
->where('status', 1)
->count();
if ($enterpriseQuestionCount > 0) {
$resolvedEnterpriseId = $enterpriseId;
}
}
$query = QuestionModel::where('type', $type)->where('status', 1);
if ($resolvedEnterpriseId !== null) {
$query->where('enterpriseId', $resolvedEnterpriseId);
} else {
$query->whereNull('enterpriseId');
}
$list = $query->order('sort', 'asc')
->order('id', 'asc')
->field('id,question,options,dimension')
->select()
->toArray();
foreach ($list as &$item) {
if (isset($item['options']) && is_object($item['options'])) {
$item['options'] = json_decode(json_encode($item['options']), true);
}
if (isset($item['options']) && is_array($item['options']) && !empty($item['options']) && !isset($item['options'][0])) {
$item['options'] = array_values($item['options']);
}
if (!isset($item['options']) || !is_array($item['options'])) {
$item['options'] = [];
}
$item['id'] = (int) ($item['id'] ?? 0);
foreach ($item['options'] as &$opt) {
if (!is_array($opt)) {
continue;
}
if (!isset($opt['value']) && isset($opt['label'])) {
$opt['value'] = $opt['label'];
}
if (!isset($opt['text']) && isset($opt['label'])) {
$opt['text'] = $opt['label'];
}
}
unset($opt);
if ($type !== 'mbti') {
unset($item['dimension']);
}
}
unset($item);
return success([
'list' => $list,
'resolvedEnterpriseId' => $resolvedEnterpriseId,
'usingSuperAdminBank' => $resolvedEnterpriseId === null,
]);
}
}