本文目录导读:

我来详细介绍PHP问卷表单后端开发的完整流程和最佳实践。
数据库设计
问卷表结构
-- 问卷主表
CREATE TABLE surveys (
id INT PRIMARY KEY AUTO_INCREMENT,VARCHAR(200) NOT NULL,
description TEXT,
status ENUM('draft', 'published', 'closed') DEFAULT 'draft',
start_time DATETIME,
end_time DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 问题表
CREATE TABLE questions (
id INT PRIMARY KEY AUTO_INCREMENT,
survey_id INT NOT NULL,
question_text TEXT NOT NULL,
question_type ENUM('single', 'multiple', 'text', 'rating', 'dropdown') NOT NULL,
is_required BOOLEAN DEFAULT TRUE,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (survey_id) REFERENCES surveys(id) ON DELETE CASCADE
);
-- 选项表(针对选择题)
CREATE TABLE options (
id INT PRIMARY KEY AUTO_INCREMENT,
question_id INT NOT NULL,
option_text VARCHAR(500) NOT NULL,
sort_order INT DEFAULT 0,
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE
);
-- 答卷表
CREATE TABLE responses (
id INT PRIMARY KEY AUTO_INCREMENT,
survey_id INT NOT NULL,
respondent_ip VARCHAR(45),
respondent_ua TEXT,
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (survey_id) REFERENCES surveys(id) ON DELETE CASCADE
);
-- 答案表
CREATE TABLE answers (
id INT PRIMARY KEY AUTO_INCREMENT,
response_id INT NOT NULL,
question_id INT NOT NULL,
answer_text TEXT,
option_id INT,
FOREIGN KEY (response_id) REFERENCES responses(id) ON DELETE CASCADE,
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE,
FOREIGN KEY (option_id) REFERENCES options(id) ON DELETE SET NULL
);
后端核心代码
配置文件和数据库连接
<?php
// config.php
class Database {
private $host = 'localhost';
private $db_name = 'survey_system';
private $username = 'root';
private $password = '';
private $conn;
public function getConnection() {
$this->conn = null;
try {
$this->conn = new PDO(
"mysql:host={$this->host};dbname={$this->db_name};charset=utf8mb4",
$this->username,
$this->password
);
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch(PDOException $e) {
echo "Connection error: " . $e->getMessage();
}
return $this->conn;
}
}
?>
获取问卷API
<?php
// api/get_survey.php
require_once '../config.php';
header('Content-Type: application/json; charset=utf-8');
$database = new Database();
$db = $database->getConnection();
$surveyId = isset($_GET['id']) ? intval($_GET['id']) : 0;
try {
// 获取问卷基本信息
$stmt = $db->prepare("SELECT * FROM surveys WHERE id = ?");
$stmt->execute([$surveyId]);
$survey = $stmt->fetch();
if (!$survey) {
http_response_code(404);
echo json_encode(['error' => '问卷不存在']);
exit;
}
// 获取问题和选项
$stmt = $db->prepare("
SELECT q.*, o.id as option_id, o.option_text as option_text
FROM questions q
LEFT JOIN options o ON q.id = o.question_id
WHERE q.survey_id = ?
ORDER BY q.sort_order, o.sort_order
");
$stmt->execute([$surveyId]);
$rows = $stmt->fetchAll();
// 整理数据结构
$questions = [];
foreach ($rows as $row) {
$questionId = $row['id'];
if (!isset($questions[$questionId])) {
$questions[$questionId] = [
'id' => $questionId,
'question_text' => $row['question_text'],
'question_type' => $row['question_type'],
'is_required' => $row['is_required'],
'options' => []
];
}
if ($row['option_id']) {
$questions[$questionId]['options'][] = [
'id' => $row['option_id'],
'text' => $row['option_text']
];
}
}
$survey['questions'] = array_values($questions);
echo json_encode([
'success' => true,
'data' => $survey
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => '服务器错误']);
}
?>
提交答卷API
<?php
// api/submit_response.php
require_once '../config.php';
header('Content-Type: application/json; charset=utf-8');
// 验证请求方法
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => '仅支持POST请求']);
exit;
}
// 获取POST数据
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['survey_id']) || !isset($input['answers'])) {
http_response_code(400);
echo json_encode(['error' => '缺少必要参数']);
exit;
}
$database = new Database();
$db = $database->getConnection();
try {
$db->beginTransaction();
// 1. 验证问卷是否存在
$stmt = $db->prepare("SELECT id FROM surveys WHERE id = ? AND status = 'published'");
$stmt->execute([$input['survey_id']]);
$survey = $stmt->fetch();
if (!$survey) {
http_response_code(404);
echo json_encode(['error' => '问卷不存在或已关闭']);
exit;
}
// 2. 创建答卷记录
$stmt = $db->prepare("INSERT INTO responses (survey_id, respondent_ip, respondent_ua) VALUES (?, ?, ?)");
$stmt->execute([
$input['survey_id'],
$_SERVER['REMOTE_ADDR'],
$_SERVER['HTTP_USER_AGENT']
]);
$responseId = $db->lastInsertId();
// 3. 验证和处理答案
foreach ($input['answers'] as $answer) {
// 验证问题存在
$stmt = $db->prepare("
SELECT q.*, s.id as survey_id
FROM questions q
JOIN surveys s ON q.survey_id = s.id
WHERE q.id = ? AND q.survey_id = ?
");
$stmt->execute([$answer['question_id'], $input['survey_id']]);
$question = $stmt->fetch();
if (!$question) {
throw new Exception("问题ID {$answer['question_id']} 不存在");
}
// 验证必填项
if ($question['is_required'] && empty($answer['value'])) {
throw new Exception("问题 '{$question['question_text']}' 是必填项");
}
// 根据题型处理
switch ($question['question_type']) {
case 'single':
// 单选:验证选项是否存在
$stmt = $db->prepare("SELECT id FROM options WHERE id = ? AND question_id = ?");
$stmt->execute([$answer['value'], $answer['question_id']]);
if (!$stmt->fetch()) {
throw new Exception("无效的选项");
}
$stmt = $db->prepare("INSERT INTO answers (response_id, question_id, option_id) VALUES (?, ?, ?)");
$stmt->execute([$responseId, $answer['question_id'], $answer['value']]);
break;
case 'multiple':
// 多选:验证每个选项
if (is_array($answer['value'])) {
foreach ($answer['value'] as $optionId) {
$stmt = $db->prepare("SELECT id FROM options WHERE id = ? AND question_id = ?");
$stmt->execute([$optionId, $answer['question_id']]);
if (!$stmt->fetch()) {
throw new Exception("无效的选项");
}
$stmt = $db->prepare("INSERT INTO answers (response_id, question_id, option_id) VALUES (?, ?, ?)");
$stmt->execute([$responseId, $answer['question_id'], $optionId]);
}
}
break;
case 'text':
// 文本:直接保存
$stmt = $db->prepare("INSERT INTO answers (response_id, question_id, answer_text) VALUES (?, ?, ?)");
$stmt->execute([$responseId, $answer['question_id'], $answer['value']]);
break;
case 'rating':
// 评分:验证范围
$rating = intval($answer['value']);
if ($rating < 1 || $rating > 5) {
throw new Exception("评分范围必须在1-5之间");
}
$stmt = $db->prepare("INSERT INTO answers (response_id, question_id, answer_text) VALUES (?, ?, ?)");
$stmt->execute([$responseId, $answer['question_id'], $rating]);
break;
case 'dropdown':
// 下拉选择:验证选项
$stmt = $db->prepare("SELECT id FROM options WHERE id = ? AND question_id = ?");
$stmt->execute([$answer['value'], $answer['question_id']]);
if (!$stmt->fetch()) {
throw new Exception("无效的选项");
}
$stmt = $db->prepare("INSERT INTO answers (response_id, question_id, option_id) VALUES (?, ?, ?)");
$stmt->execute([$responseId, $answer['question_id'], $answer['value']]);
break;
default:
throw new Exception("不支持的问题类型");
}
}
$db->commit();
echo json_encode([
'success' => true,
'message' => '提交成功',
'response_id' => $responseId
]);
} catch (Exception $e) {
$db->rollBack();
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
?>
安全措施
CSRF保护(简化版)
<?php
// secure/csrf.php
session_start();
class CSRF {
public static function generateToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public static function verifyToken($token) {
if (!isset($_SESSION['csrf_token'])) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
}
?>
XSS过滤
<?php
// secure/sanitize.php
class Sanitizer {
public static function sanitize($input) {
if (is_array($input)) {
return array_map([self::class, 'sanitize'], $input);
}
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
}
?>
前端验证示例
// submit_survey.js
async function submitSurvey(surveyId) {
const form = document.querySelector('#surveyForm');
const formData = new FormData(form);
// 收集答案
const answers = [];
form.querySelectorAll('[data-question-id]').forEach(question => {
const questionId = question.dataset.questionId;
const type = question.dataset.type;
switch(type) {
case 'single':
const selected = question.querySelector('input[type="radio"]:checked');
if (selected) {
answers.push({
question_id: questionId,
value: selected.value
});
}
break;
case 'multiple':
const checked = question.querySelectorAll('input[type="checkbox"]:checked');
if (checked.length > 0) {
answers.push({
question_id: questionId,
value: Array.from(checked).map(cb => cb.value)
});
}
break;
case 'text':
const textarea = question.querySelector('textarea');
if (textarea.value.trim()) {
answers.push({
question_id: questionId,
value: textarea.value.trim()
});
}
break;
case 'rating':
const rating = question.querySelector('input[type="range"]');
if (rating) {
answers.push({
question_id: questionId,
value: rating.value
});
}
break;
case 'dropdown':
const select = question.querySelector('select');
if (select.value) {
answers.push({
question_id: questionId,
value: select.value
});
}
break;
}
});
// 提交数据
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
const response = await fetch('/api/submit_response.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
survey_id: surveyId,
answers: answers
})
});
const result = await response.json();
if (result.success) {
alert('提交成功!');
window.location.href = '/thank-you.html';
} else {
alert('提交失败: ' + result.error);
}
} catch (error) {
console.error('Error:', error);
alert('提交失败,请稍后重试');
}
}
数据分析示例
<?php
// api/survey_analysis.php
class SurveyAnalysis {
private $db;
public function __construct($db) {
$this->db = $db;
}
public function getQuestionStats($surveyId, $questionId) {
$stats = [];
// 获取问题类型
$stmt = $this->db->prepare("SELECT question_type FROM questions WHERE id = ?");
$stmt->execute([$questionId]);
$question = $stmt->fetch();
switch($question['question_type']) {
case 'single':
case 'dropdown':
$stmt = $this->db->prepare("
SELECT o.option_text, COUNT(a.id) as count
FROM options o
LEFT JOIN answers a ON o.id = a.option_id
WHERE o.question_id = ?
GROUP BY o.id, o.option_text
ORDER BY count DESC
");
$stmt->execute([$questionId]);
$stats = $stmt->fetchAll();
break;
case 'text':
$stmt = $this->db->prepare("
SELECT answer_text, COUNT(*) as count
FROM answers
WHERE question_id = ?
GROUP BY answer_text
ORDER BY count DESC
LIMIT 20
");
$stmt->execute([$questionId]);
$stats = $stmt->fetchAll();
break;
case 'rating':
$stmt = $this->db->prepare("
SELECT answer_text, COUNT(*) as count
FROM answers
WHERE question_id = ?
GROUP BY answer_text
ORDER BY answer_text
");
$stmt->execute([$questionId]);
$stats = $stmt->fetchAll();
// 计算平均值
$avgStmt = $this->db->prepare("
SELECT AVG(CAST(answer_text AS DECIMAL)) as average
FROM answers
WHERE question_id = ?
");
$avgStmt->execute([$questionId]);
$stats['average'] = $avgStmt->fetch()['average'];
break;
}
return $stats;
}
}
?>
部署建议
- 配置环境变量:敏感信息不要硬编码
- 使用HTTPS:确保数据传输加密
- 设置CORS:如果需要跨域访问
- 限制提交频率:防止恶意刷票
- 备份数据:定期备份数据库
- 错误日志:记录错误信息到日志文件
这个完整的后端解决方案涵盖了问卷系统的核心功能,包括数据验证、安全防护和数据分析。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播