本文目录导读:

在PHP项目中实现图书借阅功能,通常需要一个完整的 MVC(模型-视图-控制器) 结构,并配合数据库来管理用户、图书以及借阅记录。
以下是一个从数据库设计到核心功能代码的完整实现方案。
第一步:数据库设计 (MySQL)
至少需要三张核心表:
-- 1. 用户表
CREATE TABLE `users` (
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL, -- 存储 password_hash() 的结果
`email` VARCHAR(100),
`status` TINYINT(1) DEFAULT 1, -- 1:正常, 0:禁用
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 图书表
CREATE TABLE `books` (
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`isbn` VARCHAR(20) UNIQUE, VARCHAR(255) NOT NULL,
`author` VARCHAR(100),
`total_quantity` INT(11) UNSIGNED DEFAULT 1, -- 总数量
`available_quantity` INT(11) UNSIGNED DEFAULT 1, -- 当前可借数量
`status` TINYINT(1) DEFAULT 1,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 借阅记录表
CREATE TABLE `borrow_records` (
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` INT(11) UNSIGNED NOT NULL,
`book_id` INT(11) UNSIGNED NOT NULL,
`borrow_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`due_date` DATETIME NOT NULL, -- 应还日期
`return_date` DATETIME DEFAULT NULL, -- 实际还书日期,NULL表示未还
`status` ENUM('borrowed', 'returned', 'overdue') DEFAULT 'borrowed',
`fine` DECIMAL(10,2) DEFAULT 0.00, -- 罚款金额
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`book_id`) REFERENCES `books`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第二步:核心功能实现 (PHP + PDO)
这里使用面向对象的方式封装核心功能。
数据库连接类 (Db.php)
<?php
class Database {
private static $instance = null;
private $connection;
private $host = 'localhost';
private $dbname = 'library_db';
private $user = 'root';
private $pass = '';
private function __construct() {
try {
$this->connection = new PDO(
"mysql:host={$this->host};dbname={$this->dbname};charset=utf8mb4",
$this->user,
$this->pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
return $this->connection;
}
}
借阅控制器 (BorrowController.php)
<?php
require_once 'Db.php';
class BorrowController {
private $db;
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
/**
* 借书操作
* @param int $userId 用户ID
* @param int $bookId 图书ID
* @param int $borrowDays 借阅天数(默认30天)
* @return array ['success' => bool, 'message' => string]
*/
public function borrowBook($userId, $bookId, $borrowDays = 30) {
try {
// 开启事务
$this->db->beginTransaction();
// 1. 检查用户是否存在且未被禁用
$userStmt = $this->db->prepare("SELECT id, status FROM users WHERE id = ?");
$userStmt->execute([$userId]);
$user = $userStmt->fetch();
if (!$user) {
throw new \Exception("用户不存在");
}
if ($user['status'] == 0) {
throw new \Exception("该用户已被禁用,无法借书");
}
// 2. 检查图书是否可借
$bookStmt = $this->db->prepare("SELECT id, available_quantity FROM books WHERE id = ? AND status = 1 FOR UPDATE");
$bookStmt->execute([$bookId]);
$book = $bookStmt->fetch();
if (!$book) {
throw new \Exception("图书不存在或已下架");
}
if ($book['available_quantity'] <= 0) {
throw new \Exception("该书当前无库存可借");
}
// 3. 检查用户借阅上限(例如每人同时最多借5本)
$countStmt = $this->db->prepare("SELECT COUNT(*) as cnt FROM borrow_records WHERE user_id = ? AND status = 'borrowed'");
$countStmt->execute([$userId]);
$borrowedCount = $countStmt->fetch()['cnt'];
if ($borrowedCount >= 5) {
throw new \Exception("您已达到最大借阅数量(5本),请先还书");
}
// 4. 执行扣减库存
$updateBook = $this->db->prepare("UPDATE books SET available_quantity = available_quantity - 1 WHERE id = ? AND available_quantity > 0");
$updateBook->execute([$bookId]);
if ($updateBook->rowCount() === 0) {
throw new \Exception("库存更新失败,可能库存不足");
}
// 5. 插入借阅记录
$borrowDate = date('Y-m-d H:i:s');
$dueDate = date('Y-m-d H:i:s', strtotime("+{$borrowDays} days"));
$insertRecord = $this->db->prepare(
"INSERT INTO borrow_records (user_id, book_id, borrow_date, due_date, status)
VALUES (?, ?, ?, ?, 'borrowed')"
);
$insertRecord->execute([$userId, $bookId, $borrowDate, $dueDate]);
// 提交事务
$this->db->commit();
return ['success' => true, 'message' => '借书成功!应还日期:' . $dueDate];
} catch (\Exception $e) {
$this->db->rollBack();
return ['success' => false, 'message' => '借书失败:' . $e->getMessage()];
}
}
/**
* 还书操作
* @param int $recordId 借阅记录ID
* @return array ['success' => bool, 'message' => string, 'fine' => float]
*/
public function returnBook($recordId) {
try {
$this->db->beginTransaction();
// 1. 获取借阅记录并加锁
$recordStmt = $this->db->prepare(
"SELECT * FROM borrow_records WHERE id = ? AND status = 'borrowed' FOR UPDATE"
);
$recordStmt->execute([$recordId]);
$record = $recordStmt->fetch();
if (!$record) {
throw new \Exception("借阅记录不存在或已归还");
}
// 2. 计算逾期罚款(假设每天0.5元)
$fine = 0.00;
$returnDate = date('Y-m-d H:i:s');
$dueDate = $record['due_date'];
if (strtotime($returnDate) > strtotime($dueDate)) {
$daysOverdue = ceil((strtotime($returnDate) - strtotime($dueDate)) / 86400);
$fine = $daysOverdue * 0.50;
}
// 3. 更新记录
$updateRecord = $this->db->prepare(
"UPDATE borrow_records
SET return_date = ?, status = 'returned', fine = ?
WHERE id = ? AND status = 'borrowed'"
);
$updateRecord->execute([$returnDate, $fine, $recordId]);
// 4. 归还库存
$updateBook = $this->db->prepare(
"UPDATE books SET available_quantity = available_quantity + 1 WHERE id = ?"
);
$updateBook->execute([$record['book_id']]);
$this->db->commit();
$msg = '还书成功!';
if ($fine > 0) {
$msg .= " 共逾期 " . ($daysOverdue ?? 0) . " 天,产生罚款:¥" . number_format($fine, 2);
}
return ['success' => true, 'message' => $msg, 'fine' => $fine];
} catch (\Exception $e) {
$this->db->rollBack();
return ['success' => false, 'message' => '还书失败:' . $e->getMessage()];
}
}
/**
* 查询用户当前的借阅列表
* @param int $userId
* @return array
*/
public function getUserBorrowedBooks($userId) {
$stmt = $this->db->prepare(
"SELECT br.*, b.title, b.isbn, b.author
FROM borrow_records br
JOIN books b ON br.book_id = b.id
WHERE br.user_id = ? AND br.status = 'borrowed'
ORDER BY br.borrow_date DESC"
);
$stmt->execute([$userId]);
return $stmt->fetchAll();
}
/**
* 检查并更新逾期状态(可由定时任务调用)
*/
public function updateOverdueStatus() {
$stmt = $this->db->prepare(
"UPDATE borrow_records
SET status = 'overdue'
WHERE status = 'borrowed' AND due_date < NOW()"
);
$stmt->execute();
return $stmt->rowCount();
}
}
第三步:使用示例 (前端调用)
<?php require_once 'BorrowController.php'; $borrow = new BorrowController(); // 用户1借阅图书2,为期14天 $result = $borrow->borrowBook(1, 2, 14); print_r($result); // ['success' => true, 'message' => '借书成功!...']
第四步:关键注意事项
- 事务与锁:
- 借书/还书操作必须放在事务中。
- 查询库存时使用
SELECT ... FOR UPDATE(行级锁),防止并发情况下超借。
- 库存逻辑:
- 不要依赖代码中的
if (available > 0)来判断,一定要在SQL中执行原子操作:UPDATE books SET available = available - 1 WHERE id = ? AND available > 0。
- 不要依赖代码中的
- 安全性:
- 所有用户输入必须使用预处理语句(PDO参数绑定)。
- 密码使用
password_hash()和password_verify()。 - API接口需要有用户身份验证(如JWT或Session)。
- 性能优化:
- 为
borrow_records.user_id、books.available_quantity添加索引。 - 使用定时任务(Cron Job)每天运行
updateOverdueStatus()来更新逾期状态。
- 为
- 扩展功能:
- 预约/续借功能:新增
reservations表,添加续借次数限制。 - 图书搜索:使用
LIKE或 Elasticsearch。 - 邮件通知:借阅成功、逾期提醒(使用PHPMailer)。
- 预约/续借功能:新增
推荐的项目结构
library-system/
├── config/
│ └── database.php
├── models/
│ ├── User.php
│ ├── Book.php
│ └── BorrowRecord.php
├── controllers/
│ ├── AuthController.php
│ ├── BookController.php
│ └── BorrowController.php
├── views/
│ ├── layout/
│ ├── book/
│ └── borrow/
├── public/
│ └── index.php (前端入口)
├── migrations/
│ └── init.sql
└── vendor/ (Composer)
这种结构可以让你轻松扩展为完整的Web应用(Laravel、Symfony等框架的实现原理与此类似)。