本文目录导读:

我来详细介绍如何实现一个功能完整的PHP笔记系统,我将从基础架构到高级功能逐步说明。
系统架构设计
目录结构
note-system/
├── config/
│ └── database.php
├── includes/
│ ├── auth.php
│ ├── functions.php
│ └── session.php
├── assets/
│ ├── css/
│ ├── js/
│ └── images/
├── views/
│ ├── login.php
│ ├── register.php
│ ├── dashboard.php
│ └── note-editor.php
├── api/
│ ├── notes.php
│ ├── categories.php
│ └── search.php
└── index.php
数据库设计
SQL建表语句
-- 用户表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP NULL
);
-- 笔记分类表
CREATE TABLE categories (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
name VARCHAR(100) NOT NULL,
color VARCHAR(7) DEFAULT '#007bff',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- 笔记主表
CREATE TABLE notes (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
category_id INT,VARCHAR(255) NOT NULL,
content TEXT,
is_pinned BOOLEAN DEFAULT FALSE,
is_archived BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL,
INDEX idx_user_created (user_id, created_at DESC),
FULLTEXT INDEX idx_search (title, content)
);
-- 标签表
CREATE TABLE tags (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
name VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_tag (user_id, name)
);
-- 笔记-标签关联表
CREATE TABLE note_tags (
note_id INT,
tag_id INT,
PRIMARY KEY (note_id, tag_id),
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
核心功能实现
1 数据库连接配置 (config/database.php)
<?php
class Database {
private $host = 'localhost';
private $db_name = 'note_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) {
error_log("Connection error: " . $e->getMessage());
return null;
}
return $this->conn;
}
}
2 笔记CRUD操作 (api/notes.php)
<?php
require_once '../config/database.php';
require_once '../includes/auth.php';
class NoteManager {
private $db;
private $user_id;
public function __construct($user_id) {
$database = new Database();
$this->db = $database->getConnection();
$this->user_id = $user_id;
}
// 创建笔记
public function createNote($title, $content, $category_id = null, $tags = []) {
try {
$this->db->beginTransaction();
$query = "INSERT INTO notes (user_id, category_id, title, content)
VALUES (:user_id, :category_id, :title, :content)";
$stmt = $this->db->prepare($query);
$stmt->bindParam(':user_id', $this->user_id);
$stmt->bindParam(':category_id', $category_id);
$stmt->bindParam(':title', $title);
$stmt->bindParam(':content', $content);
$stmt->execute();
$note_id = $this->db->lastInsertId();
// 添加标签
if (!empty($tags)) {
$this->addTagsToNote($note_id, $tags);
}
$this->db->commit();
return ['success' => true, 'note_id' => $note_id];
} catch (Exception $e) {
$this->db->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
// 获取用户的所有笔记
public function getNotes($page = 1, $per_page = 20, $category_id = null, $search = null) {
$offset = ($page - 1) * $per_page;
$query = "SELECT n.*, c.name as category_name, c.color as category_color
FROM notes n
LEFT JOIN categories c ON n.category_id = c.id
WHERE n.user_id = :user_id AND n.is_archived = FALSE";
$params = [':user_id' => $this->user_id];
if ($category_id) {
$query .= " AND n.category_id = :category_id";
$params[':category_id'] = $category_id;
}
if ($search) {
$query .= " AND MATCH(n.title, n.content) AGAINST(:search IN BOOLEAN MODE)";
$params[':search'] = $search;
}
$query .= " ORDER BY n.is_pinned DESC, n.updated_at DESC LIMIT :limit OFFSET :offset";
$stmt = $this->db->prepare($query);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->bindValue(':limit', $per_page, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
// 更新笔记
public function updateNote($note_id, $data) {
try {
$query = "UPDATE notes SET
title = :title,
content = :content,
category_id = :category_id
WHERE id = :note_id AND user_id = :user_id";
$stmt = $this->db->prepare($query);
$stmt->bindParam(':title', $data['title']);
$stmt->bindParam(':content', $data['content']);
$stmt->bindParam(':category_id', $data['category_id']);
$stmt->bindParam(':note_id', $note_id);
$stmt->bindParam(':user_id', $this->user_id);
return $stmt->execute();
} catch (PDOException $e) {
return false;
}
}
// 删除笔记
public function deleteNote($note_id) {
$query = "DELETE FROM notes WHERE id = :note_id AND user_id = :user_id";
$stmt = $this->db->prepare($query);
$stmt->bindParam(':note_id', $note_id);
$stmt->bindParam(':user_id', $this->user_id);
return $stmt->execute();
}
// 置顶/取消置顶
public function togglePin($note_id) {
$query = "UPDATE notes SET is_pinned = NOT is_pinned
WHERE id = :note_id AND user_id = :user_id";
$stmt = $this->db->prepare($query);
$stmt->bindParam(':note_id', $note_id);
$stmt->bindParam(':user_id', $this->user_id);
return $stmt->execute();
}
// 归档/取消归档
public function toggleArchive($note_id) {
$query = "UPDATE notes SET is_archived = NOT is_archived
WHERE id = :note_id AND user_id = :user_id";
$stmt = $this->db->prepare($query);
$stmt->bindParam(':note_id', $note_id);
$stmt->bindParam(':user_id', $this->user_id);
return $stmt->execute();
}
// 添加标签到笔记
private function addTagsToNote($note_id, $tags) {
foreach ($tags as $tag_name) {
// 查找或创建标签
$tag_query = "INSERT IGNORE INTO tags (user_id, name) VALUES (:user_id, :name)";
$stmt = $this->db->prepare($tag_query);
$stmt->bindParam(':user_id', $this->user_id);
$stmt->bindParam(':name', $tag_name);
$stmt->execute();
// 获取标签ID
$tag_id_query = "SELECT id FROM tags WHERE user_id = :user_id AND name = :name";
$stmt = $this->db->prepare($tag_id_query);
$stmt->bindParam(':user_id', $this->user_id);
$stmt->bindParam(':name', $tag_name);
$stmt->execute();
$tag_id = $stmt->fetchColumn();
// 关联笔记和标签
$link_query = "INSERT IGNORE INTO note_tags (note_id, tag_id) VALUES (:note_id, :tag_id)";
$stmt = $this->db->prepare($link_query);
$stmt->bindParam(':note_id', $note_id);
$stmt->bindParam(':tag_id', $tag_id);
$stmt->execute();
}
}
}
3 笔记编辑器视图 (views/note-editor.php)
<?php
require_once '../includes/auth.php';
require_once '../includes/functions.php';
if (!is_logged_in()) {
header('Location: login.php');
exit;
}
$note_id = isset($_GET['id']) ? (int)$_GET['id'] : null;
$note = null;
if ($note_id) {
$note = get_note_by_id($note_id);
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">笔记编辑器</title>
<link rel="stylesheet" href="../assets/css/editor.css">
</head>
<body>
<div class="editor-container">
<div class="editor-header">
<input type="text" id="note-title"
value="<?php echo htmlspecialchars($note['title'] ?? ''); ?>"
placeholder="输入标题...">
<div class="editor-actions">
<select id="category-select">
<option value="">选择分类</option>
<?php
$categories = get_user_categories();
foreach ($categories as $category) {
$selected = ($note && $note['category_id'] == $category['id']) ? 'selected' : '';
echo "<option value='{$category['id']}' {$selected}>{$category['name']}</option>";
}
?>
</select>
<button onclick="saveNote()" class="btn-save">保存</button>
<button onclick="archiveNote()" class="btn-archive">归档</button>
<button onclick="deleteNote()" class="btn-delete">删除</button>
</div>
</div>
<div class="editor-body">
<textarea id="note-content" placeholder="开始写作..."><?php
echo htmlspecialchars($note['content'] ?? '');
?></textarea>
</div>
<div class="editor-footer">
<div class="tags-input">
<input type="text" id="tag-input" placeholder="添加标签 (按回车)">
<div id="tags-container">
<?php if ($note): ?>
<?php foreach (get_note_tags($note['id']) as $tag): ?>
<span class="tag"><?php echo htmlspecialchars($tag); ?></span>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<div class="note-info">
<span id="word-count">0 字</span>
<span id="last-saved">最后保存: <span id="save-time">--</span></span>
</div>
</div>
</div>
<script src="../assets/js/editor.js"></script>
<script>
const noteId = <?php echo $note_id ?? 'null'; ?>;
// 自动保存功能
let autoSaveTimer = null;
const saveNote = async () => {
const title = document.getElementById('note-title').value;
const content = document.getElementById('note-content').value;
const category_id = document.getElementById('category-select').value;
const tags = getTags();
try {
const response = await fetch('/api/save-note.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: noteId,
title: title,
content: content,
category_id: category_id,
tags: tags
})
});
const result = await response.json();
if (result.success) {
updateSaveTime();
showToast('保存成功');
}
} catch (error) {
console.error('保存失败:', error);
showToast('保存失败,请重试', 'error');
}
};
// 实时字数统计
document.getElementById('note-content').addEventListener('input', function() {
const wordCount = this.value.length;
document.getElementById('word-count').textContent = `${wordCount} 字`;
});
// 标签处理
document.getElementById('tag-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const tag = this.value.trim();
if (tag && !getTags().includes(tag)) {
addTag(tag);
this.value = '';
}
}
});
function addTag(tag) {
const container = document.getElementById('tags-container');
const tagElement = document.createElement('span');
tagElement.className = 'tag';
tagElement.innerHTML = `${tag} <span onclick="this.parentElement.remove()">×</span>`;
container.appendChild(tagElement);
}
function getTags() {
const tagElements = document.querySelectorAll('#tags-container .tag');
return Array.from(tagElements).map(el => el.textContent.replace('×', '').trim());
}
</script>
</body>
</html>
高级功能实现
1 搜索功能
// api/search.php
class NoteSearch {
public function searchNotes($user_id, $query, $filters = []) {
$sql = "SELECT n.*, c.name as category_name
FROM notes n
LEFT JOIN categories c ON n.category_id = c.id
WHERE n.user_id = :user_id";
$params = [':user_id' => $user_id];
// 全文搜索
if ($query) {
$sql .= " AND MATCH(n.title, n.content) AGAINST(:query IN BOOLEAN MODE)";
$params[':query'] = $query;
}
// 日期筛选
if (!empty($filters['date_from'])) {
$sql .= " AND n.created_at >= :date_from";
$params[':date_from'] = $filters['date_from'] . ' 00:00:00';
}
if (!empty($filters['date_to'])) {
$sql .= " AND n.created_at <= :date_to";
$params[':date_to'] = $filters['date_to'] . ' 23:59:59';
}
// 分类筛选
if (!empty($filters['category_id'])) {
$sql .= " AND n.category_id = :category_id";
$params[':category_id'] = $filters['category_id'];
}
$sql .= " ORDER BY n.updated_at DESC LIMIT 50";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
}
2 导出功能
// api/export.php
class NoteExport {
public function exportToMarkdown($note_id, $user_id) {
$note = $this->getNote($note_id, $user_id);
$markdown = "# {$note['title']}\n\n";
$markdown .= "> 创建时间: {$note['created_at']}\n";
$markdown .= "> 更新时间: {$note['updated_at']}\n\n";
$markdown .= "---\n\n";
$markdown .= $note['content'];
header('Content-Type: text/markdown');
header('Content-Disposition: attachment; filename="' .
sanitize_filename($note['title']) . '.md"');
echo $markdown;
}
public function exportToPDF($note_ids, $user_id) {
require_once '../vendor/autoload.php'; // 使用mpdf或其他PDF库
$mpdf = new \Mpdf\Mpdf();
foreach ($note_ids as $note_id) {
$note = $this->getNote($note_id, $user_id);
$html = "<h1>{$note['title']}</h1>";
$html .= "<p>创建时间: {$note['created_at']}</p>";
$html .= "<hr>";
$html .= nl2br($note['content']);
$html .= "<pagebreak>";
$mpdf->WriteHTML($html);
}
$mpdf->Output('notes-export.pdf', 'D');
}
}
安全考虑
1 XSS防护
// 输出时使用htmlspecialchars
function h($string) {
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
// 存储时使用参数化查询
// 使用Content-Security-Policy头
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'");
2 CSRF防护
// 生成CSRF令牌
function generateCSRFToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// 验证CSRF令牌
function verifyCSRFToken($token) {
return hash_equals($_SESSION['csrf_token'], $token);
}
性能优化
1 缓存策略
class NoteCache {
public function getCachedNotes($user_id, $cache_key) {
$cache_file = "cache/{$user_id}/{$cache_key}.cache";
if (file_exists($cache_file) && (time() - filemtime($cache_file)) < 3600) {
return unserialize(file_get_contents($cache_file));
}
return null;
}
public function setCachedNotes($user_id, $cache_key, $data) {
$cache_dir = "cache/{$user_id}";
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
file_put_contents(
"{$cache_dir}/{$cache_key}.cache",
serialize($data)
);
}
}
这个笔记系统实现了:
- 完整的CRUD操作
- 分类和标签管理
- 搜索和筛选功能
- 自动保存功能
- 安全防护措施
- 性能优化方案
你可以根据实际需求在此基础上添加:
- 富文本编辑器(如TinyMCE、Quill等)
- 版本历史管理
- 协作编辑功能
- 移动端适配
- API接口供外部调用