本文目录导读:

我来详细介绍如何实现一个功能完整的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接口供外部调用
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播