简介面向心理学测评爱好者、HR及PHP开发者的MBTI十六型人格职业性格测试源码完整包基于荣格理论实现16种人格类型的在线评估可用于个人职业规划、团队建设、人才测评等场景学习。资源共2000个文件以JavaScript、PHP、HTML、CSS、JSON文件为主另有SQL、Shell及Markdown文档覆盖前端交互、后端逻辑、数据存储与部署说明压缩包53.14MB已有230人学习下载。源码完整无加密适配PC与H5测试环境为NginxPHP7.4MySQL5.6内含SQL初始化脚本和完整前后端模块可快速搭建答题流程测评题目涵盖职场、人际、决策等维度结果页会展示对应人格类型与职业建议。附安装教程与使用说明便于部署和二次开发通过阅读源码可掌握性格测试的题库组织、计分算法、结果匹配与自适应布局实现。需按免责声明合规使用勿用于商业运营。1. MBTI十六型人格职业性格测试源码这不是静态问卷是能跑通全流程的工程MBTI十六型人格职业性格测试源码头一次解压时我以为是几个静态 HTML 页面拼出来的问卷 Demo结果发现是一套带后台管理的 PHP MySQL 工程题目管理、答题过程、四维计分、结果报告、历史记录全都有。它能解决的实际问题很具体课程设计需要一套可复现可改的测评系统个人站点想挂一个真能算出十六型人格的工具或者公司内部想采集候选人的职业偏好又不想从零写。适合两种人一种是照着部署、拿源码当底子用的另一种是想把原逻辑拆掉、换成自己题库和报告文案的开发者。下面按我从解压到跑通再到改顺手的一步步路线写源码里有哪些坑我也一起标出来。2. 源码结构与运行环境先把 PHP MySQL 这套底子盘明白拿到源码第一步别急着配环境先看清包里的目录和文件各管什么。很多人在这一块翻车是因为直接把整个包丢到服务器结果首页白屏看日志才发现入口文件路径写的是public/index.php而服务器根目录指到了项目根目录。先花十分钟把文件结构过一遍后面能省俩小时排查时间。2.1 解压后的目录从 public 到 admin 各管一块这类源码的目录组织方式我见了不少这套包基本符合下面的结构mbti/ ├── public/ # Web 根目录入口文件 index.php 建议指向这里 │ ├── index.php # 前台首页也是答题入口 │ ├── result.php # 结果展示页接收记录 id │ └── assets/ # css / js / 图片 ├── admin/ # 后台管理独立一套登录 │ ├── login.php │ ├── question_list.php # 题库管理 │ └── record_list.php # 测试记录 ├── app/ # PHP 业务逻辑 │ ├── config.php # 数据库配置 │ ├── db.php # 数据库连接封装 │ ├── mbti.php # 计分算法类 │ └── auth.php # 后台登录会话校验 └── sql/ └── mbti.sql # 数据库初始化脚本重点看public目录。如果用 Nginxroot应该直接指向public这样配置文件和数据库凭证留在 Web 根之外少一层风险。如果 Web 根指到项目根目录虽然访问http://yourdomain/index.php也能打开首页但result.php里的相对路径会错位样式全丢页面裸奔。Apache 的DocumentRoot同理一定要指向入口文件所在目录。admin目录没有放进public说明后台和前台逻辑是分开的。很多简化版源码会把admin直接塞进public访问方便但目录一旦被扫描工具命中风险就大了。拿到包后先全局搜session_start()逐个确认admin/*.php开头都有会话检查。别把“隐藏路径”当防护这个话题我在第 5 章专门展开。2.2 数据库导入和 config 配置让源码找到 MySQLSQL 脚本在sql/mbti.sql里。导入我习惯用命令行因为 phpMyAdmin 在大文件上容易卡执行时间还会吃到 PHP 内存限制mysql -u root -p sql/mbti.sql导入完必须验证表和数据。我看过这类脚本里有两张关键表mbti_questions和mbti_records前者管题目后者管测试记录。用 SQL 数一下行数确认不是只有空表SELECT COUNT(*) AS cnt FROM mbti_questions; SELECT COUNT(*) AS cnt FROM mbti_records;如果第一张表是 0说明脚本里只有建表语句题目数据是后置 INSERT。再翻脚本有没有INSERT INTO mbti_questions常见题量是 28 道、40 道或 60 道。题量不影响算法但报告模板里十六型人格的描述必须和题库维度一致这个坑在第 3 章细讲。数据库连接配置集中在app/config.php无非四个参数加一个编码define(DB_HOST, 127.0.0.1); define(DB_NAME, mbti); define(DB_USER, root); define(DB_PASS, your_password); define(DB_CHARSET, utf8mb4);把DB_HOST写成127.0.0.1而不是localhost是我的习惯。原因在于很多 PHP 环境里localhost会走 Unix Socket而 MySQL 配置的 Socket 路径如果和 PHP-FPM 的不一致就报Connection refused。写成 IP 字面量能强制走 TCP 连接绕开这个玄学。DB_CHARSET必须用utf8mb4不要用utf8。MySQL 里的utf8实际是 utf8mb3最多支持到 BMP 字符生僻字和部分符号存进去会变成问号后面乱码排查非常难受。连接还是失败的话先在命令行验证 PHP 能不能直接连库php -r new PDO(mysql:host127.0.0.1;dbnamembti;charsetutf8mb4,root,your_password); echo ok;能输出ok问题就在 PHP 代码层或环境变量不能输出检查 MySQL 账号授权和bind-address是不是只绑了127.0.0.1。这一步能快速把问题定位到“PHP 代码”还是“MySQL 服务”。2.3 运行环境里的三个隐藏前置条件第一PHP 版本要 7.0 以上。源码里能看到??和标量类型声明的写法那是 PHP 7 才支持的放到 PHP 5.6 上会直接白屏。宝塔这类面板默认版本一般够但切换版本后要重新装pdo_mysql扩展。第二fileinfo扩展要开着后台如果做导入题库的功能会依赖它。第三allow_url_fopen保持开启部分报告模板会拉远程图片关掉之后页面头像直接裂掉。这三个条件不会写在说明文档首页很多人装完卡在空白页才一个个排查。我的建议是部署前跑一遍自检php -m | grep -E pdo_mysql|fileinfo php -v ls -l public/assets如果你的运行目录里没有public说明这套包是传统写法入口在根目录访问路径要相应调整。判断标准就一条index.php里引入文件的相对路径以哪个目录为基准那个目录就是运行根目录。不要试图用绝对路径去覆盖源码里的相对路径改出来的新权限问题比原问题还难缠。运行环境理顺之后真正花时间的是题目和计分逻辑。MBTI 测试业务核心不在页面外观而在那套二分法定向规则算得准不准。下一章直接切题库表和计分算法。3. 题目与计分逻辑把十六型人格的二分法算清楚MBTI 把人格分成四个维度外向 E 对内向 I、实感 S 对直觉 N、思考 T 对情感 F、判断 J 对知觉 P。每道题只测一个维度两个选项分别偏向该维度的一侧。四个维度各取一个字母拼成四字母类型二的四次方正好十六种人格。这个逻辑不复杂但实现起来有细节尤其是反向计分和同分处理。3.1 题库表设计不要把所有题目写死在页面里判断一份 MBTI 源码是否“完整”先看题库是不是独立表。如果题干和选项写死在 HTML 里那只是静态问卷不是测评系统。完整版的题库表结构通常长这样CREATE TABLE mbti_questions ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(500) NOT NULL COMMENT 题干, option_a VARCHAR(200) NOT NULL COMMENT 选项A, option_b VARCHAR(200) NOT NULL COMMENT 选项B, dimension CHAR(2) NOT NULL COMMENT 取值 EI/SN/TF/JP, direction TINYINT NOT NULL DEFAULT 0 COMMENT 0选项A对应维度首字母1选项A对应维度次字母, sort INT NOT NULL DEFAULT 0 COMMENT 排序越小越靠前, status TINYINT NOT NULL DEFAULT 1 COMMENT 0禁用 1启用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;direction字段是这套表设计的关键很多简化版源码没有它。举个例子题目“参加聚会后你更倾向于”选项 A“留下来和大家继续聊”选项 B“自己先回去充电”维度是 EI选 A 增加 E选 B 增加 I那么direction0因为 A 对应维度首字母 E。但如果某道题是反向设计的A 描述的是内向倾向那direction就要置为 1让选 A 时给 I 加分。没有这个字段题库一旦替换算法全乱。读取题目时按状态和排序取$stmt $pdo-query(SELECT * FROM mbti_questions WHERE status1 ORDER BY sort ASC, id ASC); $questions $stmt-fetchAll(PDO::FETCH_ASSOC);前端渲染时每个题目的单选按钮 name 用q_加题目 idvalue 用 0 或 1 对应选项 A/B。改题只动数据库不碰前端页面这才算可维护的源码结构。3.2 四维度计分实现E/I、S/N、T/F、J/P 如何取舍前端把答案 POST 给后端键是题目 id值是 0 或 1。后端计算前必须先查题库核对dimension和direction再逐题累加。核心算法可以收敛成一段 PHPpublic function calculate(array $answers): array { $scores [E0,I0,S0,N0,T0,F0,J0,P0]; $ids array_keys($answers); $in implode(,, array_map(intval, $ids)); // 生产环境建议用预处理 IN 展开这里为展示保留拼接写法 $rows $this-pdo-query( SELECT id, dimension, direction FROM mbti_questions WHERE id IN ($in) )-fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { $choice intval($answers[$row[id]]); $dimension $row[dimension]; // EI / SN / TF / JP $polarity intval($row[direction]); // 0 表示选 A 给维度首字母加分 $letter ($choice $polarity) ? $dimension[0] : $dimension[1]; $scores[$letter]; } return $scores; }逐行说几个关键点array_map(intval, $ids)把 POST 里的字符串 id 转成整数避免拼接进 SQL 的内容不可控。真实项目建议改成 PDO 预处理把IN (...)展开成占位符。答案 value 0 或 1 对应option_a或option_b这是前后端约定后端只认数字不认选项文本。$polarity表示“正向方向”。为 0 时选 0 给维度首字母加分为 1 时选 1 给首字母加分。这样反向题就不需要改维度字段只改方向值。最终分数是“计数”不是“比例”。MBTI 判定用的是二分偏好累加不要用百分比做最终判断百分比只用于展示倾向强度。很多人改错逻辑是因为把direction理解成“这题对应 E 还是对应 I”然后给每道题写死一个字母。那样也能跑但题库一旦增删前后端就会系统性跑偏。我一般保留“维度 方向”的组合而不是单一字母就是为了题库可替换。3.3 类型判定与描述映射从分数到十六型人格文案分数算完后每个维度取分高的一侧拼成四字母类型。边界情况是两个字母同分这时候需要一条稳定的规则。常见的做法是取维度首字母$type ; foreach ([EI, SN, TF, JP] as $pair) { $first $pair[0]; $second $pair[1]; if ($scores[$first] $scores[$second]) { $type . $first; } else { $type . $second; } } $report $reports[$type] ?? $reports[ISTJ];这里用同分偏向首字母。很多正式测试会用倾向强度做二次判断但 E 得 3 分、I 得 3 分的人到底是“弱外向”还是“弱内向”本质没有可靠答案。取首字母是稳定、可复现的规则。要改规则直接改这段比较逻辑。四维度组合产生十六种类型源码里通常会放在app/reports.php类型中文称号典型标签ISTJ物流师型严谨、可靠、执行力强ISFJ守卫者型细致、忠诚、守护秩序INFJ提倡者型洞察力强、理想主义INTJ建筑师型独立思考、战略眼光ISTP鉴赏家型冷静、善于拆解ISFP探险家型敏感、随和INFP调停者型理想主义、价值观驱动INTP逻辑学家型思辨、好奇心强ESTP企业家型行动派、临场发挥ESFP表演者型热情、感染力强ENFP竞选者型创造力、社交能量ENTP辩论家型敏捷、好挑战ESTJ总经理型组织者、讲效率ESFJ执政官型热心、乐于助人ENFJ主人公型领袖、利他ENTJ指挥官型果断、目标导向这个映射表是源码里最有“内容价值”的部分每个类型一般包含标题、性格特点、适合职业、写给 TA 的话。做展示页可以直接用但要注意两点一是“适合职业”只能说参考不能写“最适合”避免被当成职业定论二是页面输出时用htmlspecialchars包一层报告文案里带引号和顿号直接输出到 HTML 有脚本注入风险。逻辑算清楚了下一步把答题页面和后端链路串起来这才算完整跑通。4. 测试主流程与结果报告从答题到报告保存的完整链路整套系统的使用路径是打开首页看说明 → 进入答题页 → 逐题选择 → 提交 → 生成结果报告 → 后台可查历史记录。这条链路里最容易出问题的不是某个界面而是“答案提交后如何保证不丢、不重复、不乱序”。4.1 答题页面的题目加载与前端状态答题页通常是 PHP 直接渲染题目列表再配一段原生 JavaScript 或 jQuery 做提交校验。渲染部分大概是$questions $mbti-getEnabledQuestions(); foreach ($questions as $index $q) { echo div classquestion-card>const form document.getElementById(mbtiForm); const cards document.querySelectorAll(.question-card); function saveProgress() { const obj {}; cards.forEach(card { const id card.dataset.id; const checked card.querySelector(input[typeradio]:checked); obj[id] checked ? checked.value : null; }); localStorage.setItem(mbti_progress, JSON.stringify(obj)); } cards.forEach(card { card.querySelectorAll(input[typeradio]).forEach(radio { radio.addEventListener(change, saveProgress); }); });恢复进度时页面加载完读一次localStorage把对应 name 的 radio 置为选中状态。这里有个小坑localStorage 的键在跨浏览器、跨域名时会变部署时不要假设永远有效它只是降低刷新丢失概率的兜底手段不是数据库。提交时先校验是否答完再走 fetch 提交form.addEventListener(submit, function(e) { e.preventDefault(); const total cards.length; const answered document.querySelectorAll(input[typeradio]:checked).length; if (answered total) { alert(还有 (total - answered) 道题未完成); return; } const data new FormData(form); fetch(submit_test.php, { method: POST, body: data }) .then(r r.json()) .then(res { if (res.code 0) { localStorage.removeItem(mbti_progress); location.href result.php?id res.recordId; } else { alert(res.msg); } }) .catch(() alert(网络请求失败请重试)); });用FormData(form)直接序列化整个表单不用逐个拼接参数。answered total的校验在提交前拦截半张卷避免浪费一次后端请求。4.2 后端收题与计算接口把答卷和结果落库submit_test.php是整个流程的后端入口。它要做四件事接收基本信息、校验答题数量、调用计分算法、把原始答案和计算结果写入数据库。$name trim($_POST[username] ?? 匿名用户); $answers $_POST[answers] ?? []; if (count($answers) 10) { echo json_encode([code 1, msg 答题数量太少]); exit; } $scores $mbti-calculate($answers); $type $mbti-getType($scores); $stmt $pdo-prepare( INSERT INTO mbti_records (username, answers_json, type, scores_json, created_at) VALUES (?, ?, ?, ?, NOW()) ); $stmt-execute([ $name, json_encode($answers, JSON_UNESCAPED_UNICODE), $type, json_encode($scores, JSON_UNESCAPED_UNICODE), ]); $recordId $pdo-lastInsertId(); echo json_encode([code 0, recordId $recordId]);answers_json存的是原始答题数据为以后换算法留后路scores_json存维度原始得分结果页要展示倾向强度时不需要再算一遍。如果源码里没有answers_json字段结果页只能显示四字母不能显示每个维度的分数条报告丰富度会差一截。这里补一道防重复提交的锁。最简单的方式是提交前在session里标记上次提交时间$last $_SESSION[mbti_last_submit] ?? 0; if (time() - $last 3) { echo json_encode([code 1, msg 提交过于频繁]); exit; } $_SESSION[mbti_last_submit] time();再加一个前端按钮置灰三重保障。如果不做限制这种接口被人刷两分钟就能把你的mbti_records表写爆。4.3 结果报告展示与历史记录查询结果页result.php根据记录 id 查出数据再映射到报告文案$stmt $pdo-prepare(SELECT * FROM mbti_records WHERE id ?); $stmt-execute([$id]); $record $stmt-fetch(PDO::FETCH_ASSOC); $scores json_decode($record[scores_json], true); $report $reports[$record[type]];页面上展示四字母类型、中文称号、维度分数条、性格描述和适合职业。维度分数条我一般是并排两个进度条例如 E 和 I 放在一组用宽度表示各自得分占比。这块做得好的源码会在分数条上方标出“倾向强度62%”用scores里的两个字母算$total ($scores[E] $scores[I]) ?: 1; $ePercent round($scores[E] * 100 / $total);百分比的显示比原始计数直观但不能用它反过来改判定结果判定逻辑保持第 3 章的规则。历史记录在后台admin/record_list.php查询语句一般要支持按时间和类型筛选SELECT id, username, type, created_at FROM mbti_records WHERE type INTJ AND created_at BETWEEN 2024-01-01 AND 2024-12-31 ORDER BY created_at DESC LIMIT 20;后台列表要加导出功能CSV 导出是最简单的落地方式。导出时注意把answers_json和scores_json列去掉给客户的表只需要姓名、类型、各维度得分、提交时间。这个细节很多简化版源码会漏但实际使用越往后越需要。链路跑通后剩下的是部署和维护阶段的硬骨头。第 5 章我把最常踩的几个坑集中写出来都是真实环境里会反复出现的现象。5. 避坑指南MBTI 源码部署里的五个典型翻车点这套源码我在本地和服务器上各部署过也帮朋友排查过二次开发后的故障。下面五条都是真实出现过的每一条按照“现象 → 原因 → 解决”的顺序写方便你直接对照。5.1 数据库连不上Socket 路径和账号授权是最大的坑现象部署完成后首页能开但点提交或进后台报SQLSTATE[HY000] [2002] Connection refused。原因PHP 连接 MySQL 时用了localhost而 PHP-FPM 的 Unix Socket 路径和 MySQL 实例配置的 Socket 路径不一致绕了半天都连不上。还有一种情况是 MySQL 的bind-address只绑了127.0.0.1但代码里写的是服务器公网 IP 或容器名。解决config.php里把DB_HOST改成127.0.0.1强制走 TCP 回环。然后手动跑一次连接测试php -r new PDO(mysql:host127.0.0.1;dbnamembti;charsetutf8mb4,root,your_password); echo ok;还不行就查 MySQL 账号权限SELECT user, host FROM mysql.user WHERE user root; GRANT ALL PRIVILEGES ON mbti.* TO root127.0.0.1 IDENTIFIED BY your_password; FLUSH PRIVILEGES;血泪经验不要在没确认连接方式前就重置 MySQL 密码绕远路。5.2 计分结果跑偏先查维度映射再查同分规则现象同一道题选 A 或选 B最终类型和预期完全相反或者全卷 28 题算出连续三个同样的四字母类型明显不符合常理。原因题库里direction字段标错了。部分题目是反向设计的direction没有跟着改选 A 本来该加 I结果加到了 E。还有一种原因是结果页读scores_json时键名用错源码里有的键是E/I有的是EI有的又是单个字母E。解决先用 SQL 抽查每个维度的正反向分布SELECT dimension, direction, COUNT(*) FROM mbti_questions GROUP BY dimension, direction;正常情况每个维度下两个direction值都有分布如果某个维度全是 0说明反向题没标出来。逐题核对后更新UPDATE mbti_questions SET direction 1 WHERE id 37;改完要重测。我的做法是写一组固定答案集把 16 种类型都覆盖到跑完以后每个类型至少验证一次。同分规则也要单独看和会让边界用户落进不同类型选哪种都要在代码注释里写清楚不然三个月后自己都看不懂。5.3 中文乱码统一 utf8mb4 与环境变量现象后台题目中文正常但新录入的题目提交后前台显示问号或者从 phpMyAdmin 导出 SQL 再导入到另一台服务器后全部乱码。原因表字符集不是 utf8mb4连接字符串没带 charset导出工具默认字符集选成了 latin1。三层任何一层不一致中文就保不住。解决先改表ALTER TABLE mbti_questions CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;再确认config.php里的DB_CHARSET是utf8mb4。PHP 的 PDO 连接字符串也要带上charsetutf8mb4。数据已经在库里变成乱码的不要直接在原表上改字符集先备份再导出导出时用--default-character-setutf8mb4mysqldump -u root -p --default-character-setutf8mb4 mbti mbti_backup.sql导入时同样带参数mysql -u root -p --default-character-setutf8mb4 mbti mbti_backup.sql以后无论用 phpMyAdmin 还是命令行统一 utf8mb4 一条路走到黑。5.4 后台菜单点击 404 或 403伪静态规则和目录权限现象后台能登录但点“题目管理”跳到 404或者浏览器直接把.php文件下载下来了。原因Nginx 的location配置没有优先处理.php请求伪静态规则把所有非实际文件请求 rewrite 到了index.php导致admin/question_list.php被当成前端路由。Apache 环境下则是没开mod_rewrite或.htaccess规则冲突。解决Nginx 的server块里.php$的 location 要放在 rewrite 规则之前location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location / { try_files $uri $uri/ /index.php?$query_string; }403 一般是目录权限或open_basedir限制。给app、admin目录 755 权限确认 PHP-FPM 的运行用户能读取文件。不要图省事chmod 777那是给服务器留后门。5.5 后台接口可被绕过不要只靠隐藏入口现象退出登录后直接输入admin/question_list.php地址页面照样能打开数据全暴露。原因源码只在菜单层判断登录具体文件开头没有包含auth.php或者auth.php里对 session 的判断逻辑写错了。解决在每个 admin 下的 PHP 文件顶部加统一校验require_once __DIR__ . /../app/auth.php; require_login();require_login()是一个强制跳转函数内部检查固定 session 键没有登录就直接header(Location: login.php)并exit。不要用if (!$_SESSION[isAdmin])这种裸判断session 键名要和后台登录写入时完全一致。这一步做完再扫一遍有没有文件上传接口上传接口也要过登录校验。这五条坑基本覆盖了从环境到逻辑再到安全的主要翻车点。最后一章我加一个结果页的“提质感”小功能读源码时你会更清楚哪里可以动。6. 进阶给结果页补一张雷达图让报告更像一份职业测评四字母结果页如果只有文字描述用户很难直观看到自己在每个维度上的倾向强度。我拿到这套源码后做的第一个小改动就是在结果页加一张四维雷达图数据直接从scores_json来不动判定逻辑。页面引入 Chart.js 的 CDN在result.php里放一个canvascanvas idradarChart width360 height300/canvas然后从record[scores_json]里读原始分数计算每个维度的首字母占比const raw ? json_encode($scores, JSON_UNESCAPED_UNICODE) ?; const labels [E/I, S/N, T/F, J/P]; const data labels.map(pair { const a raw[pair[0]] || 0; const b raw[pair[1]] || 0; return Math.round(a * 100 / Math.max(a b, 1)); }); const ctx document.getElementById(radarChart).getContext(2d); new Chart(ctx, { type: radar, data: { labels: labels, datasets: [{ label: 维度倾向强度, data: data, backgroundColor: rgba(54, 162, 235, 0.2), borderColor: rgba(54, 162, 235, 1), pointRadius: 3 }] }, options: { scales: { r: { min: 0, max: 100 } } } });雷达图每个顶点是左右两个字母的分数比例50 表示居中越靠 100 越倾向维度首字母。这样用户能一眼看出自己是“轻微偏 E”还是“极端偏 N”比单纯一个四字母结论信息量更大。改造完必须验证找五份历史记录对比雷达图极值和四字母类型是否一致。比如某个人是 ISTJ雷达图上 E/I 顶点应该在接近 I 的方向S/N 顶点接近 S以此类推。如果没有历史数据就用第 3 章准备好的固定答案集跑一遍逐类型核对。我从第一次部署这套 MBTI 源码到现在最大的教训就是改完算法或展示层不要只测一组数据就下结论。从那以后我每次动这个包都会强制跑一遍覆盖十六种类型的固定答案集再肉眼对一遍雷达图和四字母。希望帮到你。本文还有配套的精品资源点击获取