
简介面向K12心理教育场景的心理监测系统期末大作业项目使用PHP、HTML与MySQL协同完成学生心理问卷填写、评估结果查询和用户权限管理适合Web开发初学者或需要完成课程设计的学生参考。压缩包共27个文件包含12个PHP后端逻辑脚本、8个HTML前端页面、1个SQL数据库初始化脚本以及少量图片与项目配置整体仅199KB虽然体积不大但已覆盖登录验证、患者与医生管理、问卷提交、结果展示等核心功能模块。从内容预览来看PHP脚本与HTML页面基本一一对应文件命名直观配合SQL初始化脚本可以快速导入数据并运行还包含部分界面图片和项目配置便于在课程设计或答辩准备中定位代码位置、梳理整体业务流。已有202人学习下载能帮助读者直观理解前端表单、后端处理与数据库读写之间的协作关系是一份轻量、完整且具有实操价值的期末大作业样例。整体压缩包虽小但目录结构清晰适合作为学习网页表单、服务端逻辑与数据库交互的入门级完整项目。1. 用PHPHTMLMySQL拼一个心理监测系统期末大作业的实用分几步走深夜把MySQL服务启动又在浏览器里敲下localhost对做PHP期末大作业的人来说这是项目第一个真实动作。基于PHPHTMLMySQL实现的心理监测系统是课程里很典型的组合需求HTML提供页面结构从表单收集题目选择PHP负责会话与计算把答案变成得分MySQL存用户、量表和每次测评记录。完整路线不复杂定主链路、建三张核心表、写登录与答题流程、本地部署验证。下面按这个顺序把每一步涉及的参数、命令和坑位讲清楚。适合学过PHP基础语法但第一次做完整小系统的同学也适合下载过现成php源码却在答辩时被老师一句“这个函数哪来的”问住的人。2. 心理监测系统先想清楚为什么是PHP原生MySQL而不是框架2.1 业务主链路注册、答题、计分、历史报告一个期末心理监测系统通常会把主链路收敛成五段用户注册登录、量表列表、答题页、得分计算、历史报告。注册登录保存账号和密码哈希用来识别每次测评归属到哪个用户量表列表展示可用的自评问卷例如焦虑自评量表SAS和抑郁自评量表SDS答题页按题目顺序渲染单选框提交后按量表计分规则生成标准分和结论等级历史报告把历次测评按时间倒序展示供用户对比状态变化。把这条链路拆开看数据库里其实只有三类核心数据用户、量表定义、答题记录。量表定义继续拆成量表主表和题目明细表因为一个量表包含多道题一道题四个选项而选项对应的分值又可能按量表不同而变化甚至同一张量表里还有反向计分题所以选项分值不能写死在前端必须跟着题目一起存进MySQL。这个模型一旦定下来后面的页面和函数都是在围绕它转。2.2 为什么用PHP原生不是Laravel/TP课程要求通常是“PHPMySQL”用原生PHP反而容易拿分。原生PHP不需要composer安装不需要容器一个XAMPP或者php -S内置服务器就能跑Laravel、ThinkPHP这类框架会把目录层级拉深答辩时老师看到的可能大半是脚手架代码而不是你自己的业务逻辑。原生PHP把HTML模板直接混排在页面里老师能看到一段foreach如何把题目渲染成表单也更能对应课程里讲的PHP基础语法、运算符和函数调用。不推荐ORM的原因也很简单期末作业数据量小PDO预处理语句足够而且能在现场解释SQL参数绑定用模型层反而容易被追问连接池、迁移、查询构造器等概念。选型上我一般建议本机跑用PHP内置服务器演示时用Apache或nginx都可以MySQL一定要用InnoDB引擎并显式写成utf8mb4字符集前端不引框架原生HTML加少量JavaScript最多引一个Chart.js画趋势线。这样整个系统每一行代码都在你掌控范围内调试时不用翻三层依赖。2.3 项目目录把代码按模块而不是按类型分我比较推荐这种目录结构入口和公共逻辑分开后面扩展管理端时不用翻乱psych-system/ ├── public/ # 对外访问入口 │ ├── index.php # 量表列表 │ ├── assessment.php # 答题页 │ ├── save_result.php # 接收答案并计分 │ └── my_reports.php # 历史报告 ├── src/ # 被require的公共代码 │ ├── db.php # PDO连接 │ ├── auth.php # 登录状态检查 │ ├── scoring.php # 计分函数 │ └── util.php # 跳转、CSRF等小工具 ├── assets/ # 静态资源 │ ├── style.css │ └── chart.umd.js └── sql/ └── schema.sql # 建库建表脚本public放浏览器能直接访问的PHP文件src放被页面顶部require的工具函数assets放静态文件sql放建表脚本。这样组织的原因是将来如果从Apache换到nginx只需要把站点根目录指向public防止PHP源文件被当成静态文件下载。但如果你用php -S做本地开发直接指定-t public时静态资源会按真实路径查找想让所有动态请求都走一个入口则需要配一个router.php在router里判断文件是否存在存在就return false交给内置服务器处理不存在就require index.php。下面这张表是我写代码时对照的职责清单文件职责被谁引用public/index.php登录后的量表列表浏览器直接访问public/assessment.php渲染答题表单浏览器直接访问public/save_result.php校验答案、计分入库表单POST提交public/my_reports.php历史报告列表浏览器直接访问src/db.php返回PDO单例所有后端页面统一requiresrc/auth.php检查登录态未登录跳走需要登录的页面2.4 数据库连接层的取舍连接用PDO比mysqli更适合这个场景预处理占位符更直观异常模式能快速暴露SQL问题代码风格也更接近以后接触其他语言时的习惯。把连接集中到src/db.php所有页面统一require避免在十几个文件里复制root密码。?php // src/db.php declare(strict_types1); date_default_timezone_set(Asia/Shanghai); function db(): PDO { static $pdo null; if ($pdo instanceof PDO) { return $pdo; } $dsn mysql:host127.0.0.1;port3306;dbnamepsych;charsetutf8mb4; $pdo new PDO($dsn, root, 你的密码, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]); return $pdo; }charsetutf8mb4写在DSN里比执行语句里再SET NAMES更可靠ATTR_EMULATE_PREPARES设为false让MySQL端做真正的预处理避免PHP模拟预处理时把整型参数全部变成字符串影响WHERE id这类条件的索引命中。static变量在这里做单例避免一个请求生命周期里重复创建PDO连接。root密码不要写死在代码里课程演示可以把db.php放在src目录至少它不会直接暴露在public根下如果更讲究一点用getenv读取环境变量或者读config.php里的配置数组。3. MySQL表结构设计量表、题目、记录三张表的建表SQL与连接规范3.1 表结构一张用户表加三张业务表先定四张表users、scale、question、assessment_record。users负责账号和基础信息scale描述一种量表比如焦虑自评量表SASquestion是量表下的每一道题选项分值用JSON列保存assessment_record是用户完成某一次测评后的结果把这次的原题答案和得分一起留下。表关键字段说明usersid, username, password_hash, nickname, gender, age, created_atpassword_hash长度预留255scaleid, scale_name, scale_code, description, scoring_rulescoring_rule存JSON记录标准分换算系数questionid, scale_id, statement, option_value_json, sort_order选项分值JSON支持单独反向计分assessment_recordid, user_id, scale_id, raw_score, std_score, result_level, answer_json, created_atanswer_json保存本次答案便于回看把题目选项分值放在JSON列而不是再拆一张选项子表好处是代码里可以直接json_decode然后取[A]不需要多表关联缺点是统计“某道题选B的人数”时不方便。期末作业不做数据分析JSON列完全够用。MySQL 5.7以上原生支持JSON类型8.0还能对JSON里的虚拟列建索引。字段类型上id用无符号整数主键用户名加唯一索引密码字段预留255而不是60因为以后换用其他哈希算法时60会不够。3.2 建表SQL表结构可以直接在phpMyAdmin或MySQL Workbench里执行。执行前先确认连接账号有建库权限并且理解每条语句的含义因为老师可能会现场让你在数据库里加一个字段。MySQL安装配置教程里反复强调的字符集问题这里统一用utf8mb4和utf8mb4_unicode_ci可以同时避开中文乱码和插入emoji报错。-- sql/schema.sql CREATE DATABASE IF NOT EXISTS psych DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE psych; CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password_hash VARCHAR(255) NOT NULL, nickname VARCHAR(50) DEFAULT , gender TINYINT NOT NULL DEFAULT 0, age TINYINT UNSIGNED NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE scale ( id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, scale_name VARCHAR(100) NOT NULL, scale_code VARCHAR(30) NOT NULL, description TEXT, scoring_rule JSON NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT心理量表主表; CREATE TABLE question ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, scale_id SMALLINT UNSIGNED NOT NULL, statement VARCHAR(500) NOT NULL, option_value_json JSON NOT NULL, sort_order TINYINT UNSIGNED NOT NULL DEFAULT 0, INDEX idx_scale_sort (scale_id, sort_order) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目明细表; CREATE TABLE assessment_record ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, scale_id SMALLINT UNSIGNED NOT NULL, raw_score SMALLINT UNSIGNED NOT NULL DEFAULT 0, std_score SMALLINT UNSIGNED NOT NULL DEFAULT 0, result_level VARCHAR(20) NOT NULL, answer_json JSON NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_created (user_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT测评结果记录;执行后重点关注三处索引users.username的唯一索引保证登录名不重复登录时按用户名查找能直接命中question表的idx_scale_sort是复合索引覆盖取题目列表时的WHERE scale_id? ORDER BY sort_orderassessment_record表的idx_user_created支撑“我的报告”按用户和时间倒序也支撑后面防重复提交的时间段校验。这三个索引是本系统最频繁的查询路径。创建索引时不要为每一列都单独建复合索引已经能同时覆盖多个筛选条件冗余索引反而拖慢写入。3.3 插入量表数据和题目建表之后要往里放题目。插入时要特别注意量表里的反向题目SAS有部分题目是“我觉得一切都很好”这类正面描述反向计分规则是选A得4分而不是1分。我一般先插量表主表拿到自增ID再插入题目。-- 先插量表主表 INSERT INTO scale (scale_name, scale_code, description, scoring_rule) VALUES (焦虑自评量表, SAS, 用于评定当前焦虑状态的主观感受, JSON_OBJECT(factor, 1.25)); SET scale_id LAST_INSERT_ID(); -- 题目按标准顺序插入第4题是反向计分 INSERT INTO question (scale_id, statement, option_value_json, sort_order) VALUES (scale_id, 我觉得比平时容易紧张和着急, JSON_OBJECT(A,1,B,2,C,3,D,4), 1), (scale_id, 我无缘无故地感到害怕, JSON_OBJECT(A,1,B,2,C,3,D,4), 2), (scale_id, 我容易心里烦乱或觉得惊恐, JSON_OBJECT(A,1,B,2,C,3,D,4), 3), (scale_id, 我觉得一切都很好不会发生什么不幸, JSON_OBJECT(A,4,B,3,C,2,D,1), 4);JSON_OBJECT函数比手工写JSON字符串更不容易漏引号。LAST_INSERT_ID()只在当前连接里有效如果你换一个会话执行后面的INSERT必须先用SELECT id FROM scale WHERE scale_codeSAS查出真实ID。实际SAS有20道题这里只给4条示意完整题目要按标准逐题核对不要图省事把第4题的规则复制到所有题目上。3.4 PHP端保存测评结果计分完成之后把答案数组编码成JSON连同粗分、标准分一起写回assessment_record。JSON列最重要的配套参数是连接字符集必须为utf8mb4否则中文会乱码甚至报Incorrect string value。// save_result.php 保存片段 $answerJson json_encode($answerRaw, JSON_UNESCAPED_UNICODE); $stmt db()-prepare( INSERT INTO assessment_record (user_id, scale_id, raw_score, std_score, result_level, answer_json, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW()) ); $stmt-execute([ $_SESSION[uid], $scaleId, $rawScore, $stdScore, $level, $answerJson, ]);json_encode加上JSON_UNESCAPED_UNICODE后中文会以原始字符存入JSON列而不是转成\uXXXX。预处理绑定能避免答案里的单引号、HTML标签破坏SQL结构。如果这学期刚学过SQL注入这个点正好可以在答辩时讲清楚所有外部输入都经过预处理不手工拼SQL。3.5 要不要用存储过程如果你在MySQL课上学过存储过程可能会想把计分规则放进去比如写一个count_assessment_score(scale_id, answer_json)的存储过程然后PHP调用它。常见做法确实可以但期末大作业我不建议这样老师要看的是PHP逻辑把换算公式塞进数据库答辩时要解释两套代码调试也更麻烦。存储过程更适合做批量统计比如统计所有用户的平均标准分、某个量表的完成人数这些是管理端加分项。核心的计分逻辑留在PHP里函数就是独立可测单元将来在IDE里也能对scoring.php做单步调试。4. 从HTML表单到PHP计算问卷提交、得分计算与前端竞态4.1 页面流程三个PHP入口答题链路里最关键的是三个入口。assessment.php根据scale_id读取题目并渲染表单save_result.php是表单提交地址负责校验、计分、入库然后重定向到my_reports.phpmy_reports.php查询当前用户的所有记录并预留一个JSON变量供前端画图。写这些页面时记住一条原则业务逻辑放在HTML输出之前所有header(Location:)跳转都要发生在页面最顶部否则前面任何空行或print输出都会导致“headers already sent”错误。PHP错误处理上开发阶段把display_errors设为On答辩演示前关掉只保留错误日志避免把SQL异常直接暴露给用户。4.2 答题页HTMLdoctype、循环题目和radio答题页是一个标准HTML5文档文档必须从 开始lang改成zh-cnmeta声明utf-8。PHP循环输出题目时radio的name统一用answer[题目ID]这样提交后$_POST[answer]是一个以题目ID为键的关联数组遍历起来非常自然很多php源码处理接口数据时也会返回类似以ID为键的数组结构遍历逻辑完全一致。!-- assessment.php 模板部分 -- !DOCTYPE html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title心理测评 - 焦虑自评量表/title link relstylesheet hrefassets/style.css /head body form idquizForm methodpost actionsave_result.php input typehidden namescale_id value? htmlspecialchars((string)$scaleId, ENT_QUOTES) ? ?php foreach ($questions as $i $q): ? div classcard p? ($i 1) ?. ? htmlspecialchars($q[statement], ENT_QUOTES) ?/p labelinput typeradio nameanswer[? (int)$q[id] ?] valueA required 没有或很少/label labelinput typeradio nameanswer[? (int)$q[id] ?] valueB 有时/label labelinput typeradio nameanswer[? (int)$q[id] ?] valueC 大部分时间/label labelinput typeradio nameanswer[? (int)$q[id] ?] valueD 绝大多数时间/label /div ?php endforeach; ? button typesubmit classbtn-primary提交测评/button /form /body /html这个文件里的短标签 等价于 在PHP 5.4以后默认开启。htmlspecialchars把题目里的HTML标签转义防止量表描述中的尖括号被浏览器解析成新标签。required加在整组radio的第一个选项上浏览器会强制用户对每组都做选择省掉大量自定义JS校验。hidden字段里的scale_id也做了转义避免攻击者在URL参数里塞特殊字符后原样回显到页面。4.3 前端竞态防双击、提示未完成项测评场景一般不允许反复提交前端要做两件事提交前确认提交后把按钮禁用。如果不想用confirm也可以用页面上的div弹窗做二次确认但confirm代码量最少演示效果也够用。// 放在表单末尾的script标签中 document.querySelector(#quizForm).addEventListener(submit, function (e) { const btn this.querySelector(button[typesubmit]); if (btn.disabled) { e.preventDefault(); return; } if (!confirm(确认提交本次测评提交后无法修改。)) { e.preventDefault(); return; } btn.disabled true; btn.textContent 正在计算...; });addEventListener在表单提交事件上挂一个函数第一次触发时先检查按钮状态再弹确认框确认后立刻禁用按钮。disabled状态下浏览器会阻止再次submit。这里隐藏着一个前端竞态如果用户手速极快在confirm弹出瞬间又按回车第二次submit事件可能在btn.disabled赋值之前触发所以在事件开头还要再判断一次disabled。真正的防重复不能只靠前端必须看后端。4.4 PHP端计算得分粗分换算标准分计分逻辑放在save_result.php顶部。SAS量表的标准分是intval(粗分*1.25)也就是只取整数部分不能用round四舍五入否则59.4会被约成60结论从轻度跳到中度。PHP里用复合运算符逐题累加就是最常用的基础语法。// save_result.php 顶部 $scaleId (int)($_POST[scale_id] ?? 0); $answerRaw $_POST[answer] ?? []; if ($scaleId 0 || count($answerRaw) 1) { exit(题目不能为空); } $questions db()-prepare(SELECT id, option_value_json FROM question WHERE scale_id ?); $questions-execute([$scaleId]); $optionMap []; foreach ($questions as $q) { $optionValues json_decode($q[option_value_json], true); $optionMap[$q[id]] $optionValues; } $rawScore 0; foreach ($answerRaw as $qid $opt) { $qid (int)$qid; if (!isset($optionMap[$qid][$opt])) { continue; } $rawScore (int)$optionMap[$qid][$opt]; } $stdScore intval($rawScore * 1.25); if ($stdScore 50) { $level 正常; } elseif ($stdScore 60) { $level 轻度焦虑; } elseif ($stdScore 70) { $level 中度焦虑; } else { $level 重度焦虑; }第一步把scale_id从字符串转成整数顺便把0值挡出去。遍历answerRaw时用isset先判断题目ID是否在选项映射里避免用户自己拼一组不存在的题目ID进来continue跳过那些无效答案。计算标准分用intval而不是round。结论等级按从轻到重的顺序排列if/elseif的顺序直接决定分级结果先判断50以下再判断60以下最后判断70以下。这套逻辑适合单独抽到src/scoring.php里做成函数my_reports.php里也可以复用来判断历史结果。下面这个表是SAS标准分的分级阈值写文档时可以直接抄标准分结论50正常50~59轻度焦虑60~69中度焦虑70重度焦虑4.5 把重复提交堵在数据库层前端防双击挡不住直接改URL的人也挡不住F5刷新后浏览器再次提交表单。更可靠的重复提交防护分两层一层是CSRF token登录时在session里放一个随机串提交时放进hidden字段后端比对后才处理数据另一层是数据库层面的业务约束。常见做法是插入前先查最近记录SELECT COUNT(*) FROM assessment_record WHERE user_id ? AND scale_id ? AND created_at NOW() - INTERVAL 5 MINUTE;这里的业务约束是“5分钟内不能重复提交同一个量表”避免用户反复做同一份问卷刷出大量记录。查询能走assessment_record上的idx_user_created复合索引因为条件里同时有user_id和created_at记录量不大时性能没有任何压力。这条SQL配合后端判断比单纯禁用按钮可靠得多答辩时也更能体现你考虑过并发问题。5. 部署到PHP内置服务器和XAMPPMySQL连接报错修复与加分项5.1 本地跑通的最小命令如果只用PHP内置服务器在项目根目录执行php -S 127.0.0.1:8080 -t public然后浏览器访问http://127.0.0.1:8080/assessment.php?scale_id1。这种方式不需要装Apache但静态资源按真实路径查找如果希望所有动态请求走一个入口加一个最小router// public/router.php $file __DIR__ . / . parse_url($_SERVER[REQUEST_URI], PHP_URL_PATH); if ($file ! __DIR__ . / file_exists($file)) { return false; } require __DIR__ . /index.php;然后命令变成php -S 127.0.0.1:8080 -t public public/router.phpreturn false时PHP内置服务器会自己处理静态文件其他请求进index.php。用XAMPP则更简单把psych-system放到htdocs目录启动Apache和MySQL访问http://localhost/psych-system/public/index.php。第一件事先确认3306端口没被占用页面报connection refused时多半是MySQL服务没起来或者端口被改了。5.2 MySQL连接报错三例第一个是Access denied for user rootlocalhost。密码与DSN不一致或者root账号的host限制只能在特定网段登录。建议不用root先建一个专用账号CREATE USER psych_userlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; GRANT ALL PRIVILEGES ON psych.* TO psych_userlocalhost; FLUSH PRIVILEGES;第二个是PHP 8连接MySQL 8时报“this authentication plugin is not supported”。MySQL 8默认用caching_sha2_password老版本PHP的mysqlnd不支持所以创建账号时指定mysql_native_passwordPHP 8.1以上已经能支持但课程机房的PHP版本未必跟上。另一种解法是ALTER USER修改已有用户的认证插件改完重启Apache。第三个是中文乱码。建表用utf8mb4之后连接DSN也必须写charsetutf8mb4页面meta charsetutf-8同样不能少。在MySQL命令行里执行SHOW VARIABLES LIKE character_set_connection;看到utf8mb4就说明连接层没有问题了。这个排查路径比在PHP里反复加SET NAMES更直接。5.3 加分项在报告页导出CSV和画趋势线把历史报告导出成CSV是期末最容易拿分的功能之一。fputcsv配合BOM头可以在Excel里直接打开不乱码// my_reports.php 导出分支 header(Content-Type: text/csv; charsetutf-8); header(Content-Disposition: attachment; filenamereports.csv); echo \xEF\xBB\xBF; // 输出BOMExcel识别UTF-8 $out fopen(php://output, w); fputcsv($out, [时间, 量表, 标准分, 结论]); // 遍历结果逐行fputcsv fclose($out);需要视觉冲击时把历史标准分传给Chart.js画折线。PHP在查询记录后把分数拼成JSON数组前端new Chart($ctx, {type: line, data: {labels, datasets}})不需要后端生成图片也不依赖服务器装字体。最后提醒一句心理监测系统的结果只用于课程演示和数据管理不构成医疗诊断依据页面底部可以固定显示这个提示。提示测评结果只用于教学演示不构成任何医疗诊断依据页面里记得展示这行说明。验收时走一条完整路径注册一个新账号进入SAS量表提交三次测评打开phpMyAdmin查看assessment_record表应该看到三条created_at递增且answer_json非空的记录如果记录缺失或报错优先查看PHP错误日志和MySQL错误日志这两个日志会把问题直接指到具体行。本文还有配套的精品资源点击获取