基于PHP、MySQL实现的web端借还书系统
1.说明
php程序设计课程大作业------基于PHP、MySQL的web端借还书系统
2.TODO
- 数据库设计
- 用户注册页面
- 用户登陆页面
- 主页面
- 借书
- 还书
- 个人信息管理
- 管理员部分
- 公告
- reCAPTCHA验证验证码校验登录
- 模糊查询
- 快到期图书提醒
- 个人历史纪录
- 通过邮箱找回密码(使用了PHPMailer,简直是救星)
- 系统说明
- 统计模块(可以用Echarts生成图表,但由于只是DEMO,意义不是特别大,主要也没啥数据,生成的图表看起来很一般)
- 代码重构与优化
项目中很多都可以用vue重构,但是因为不是很熟悉,为了不要让自己做的太慢,所以基本都是用jQuery。但是如果都用vue应该会精简很多
3.借还书系统系统说明
3.1如何部署
fork
或clone
本项目;- 更改相关的数据库信息(
t/db.php
); - 配置你自己的邮件发送模块(
t/neteast_mailter.php
); - 做完上述两步后将php文件放到安全的位置并更改
php/bd_fns.php
中引用的链接; - 使用
createdb.sql
文件创建本项目的数据库:
-
- 在命令行下使用命令
mysql --u用户名 --p密码 <【sql脚本文件路径】
或 - 在MySQL控制台中使用命令
source 【sql脚本文件路径】
- 在命令行下使用命令
- 申请Google reCAPTCHA的API key并填入相关代码(如不用验证功能可以跳过此步骤并且删除相关代码);
- 保持文件夹的结构复制到你所需要的目录(不要忘了删除文件夹
t
)。
3.2结构说明
本项目每一个页面均对应1-2个css、js文件以及后台的php文件,html、css和js的文件名是同名的,可以清晰地看出对应关系,其中:
css
文件夹中存放了所有的.css文件,js
中存放了所有的.js文件account
文件夹中存放的是用户注册、登录、历史纪录以及个人信息的页面;b_and_r
文件夹中存放的是借还书的页面img
文件夹中存放的是导航栏下面的背景图片的图片文件lib
文件夹中存放了后台PHP共同使用的函数db_fns.php
和两个PHPMailer的库文件php
文件夹中的文件较为复杂,但都以功能命名
3.3测试系统环境
- Ubuntu 16.04.1
- Apache 2.4.18
- PHP 7.0.33
- MySQL 5.7.25
4.功能描述
- 整体设计思路:
-
- 所有用户均可访问主页面和借书页面以查询想要的图书信息;
- 主页面主要是一个搜素框用户搜索图书,以及一个显示公告的作用;
- 借书和还书的操作必须登录后使用;
- 读者可以查看历史记录和个人信息
- 管理员可以发布公告
- 用户信息管理模块:
-
- 注册功能。前后端均设置了表单验证和安全防范;
- 登录功能。使用了reCAPTCHA验证系统,支持使用邮箱找回密码的功能;
- 历史纪录。用户可以查看借还书的历史记录,包括书名、作者、分类、出版社等图书基本信息以及借阅时间、归还时间、超期处罚金额等信息,其中绿色表示按时归还,红色表示超期归还;
- 个人信息管理。包括三个大部分,分别是账户、安全和借阅:
-
-
- 账户:包括显示学号、姓名、性别、年级、专业、手机和电子邮箱等信息,用户可以修改手机号和电子邮箱地址;
- 安全:包括了更改密码的功能;
- 借阅:包括显示借书次数、超期次数和信用等级。信用等级可以用作提升或减少用户可借图书本数和借阅时长,这个功能暂时没有实装;
-
- 借还书功能
-
- 借书:
-
-
- 用户可以通过书名、作者、分类三个维度搜索图书,每个维度都支持模糊搜索(关键字查询),但不同维度之间没有实现此功能;
- 搜索出的结果包含了书名、作者、分类、出版社、复本数以及剩余数等信息;
- 用户在检索到想要看的图书后直接点击"借阅"按钮即可完成借阅,若用户已经借阅了三本图书未归还或有超期未归还的图书不得借阅新的图书;
- 本项目使用的图书数据超过12万条,但由于对于豆瓣网站的图书分类方法不清楚,且没有注意数据的精处理,有许多相同的图书但分类不同的图书被认为是不同的图书,但是不影响使用(毕竟没有真书),在基础数据上,将数据复制三份,以达到每本图书都有三本复本的要求;
-
-
- 还书:
-
-
- 在还书页面,用户可以看到自己正在借阅的图书的信息,包括书名、作者、分类、出版社、借阅时间、最晚归还时间和超期处罚金额等信息;
- 淡蓝色显示的是未超期的图书,红色显示的是超期的图书,对于未超期图书直接点击归还即可归还,对于超期图书需要点击缴费并归还,此时会有一个alert窗口以代替付款的流程;
- 若用户有在24小时内超期的图书,在导航栏的"还书"按钮旁会显示即将超期的图书的数量以达到提示用户的作用;
-
- 管理员系统:
-
- 管理员使用发放的账号和密码,不可通过注册页面注册;
- 管理员登录和普通用户使用相同的登录页,并且没有多余的操作;
- 管理员登录后所看到的导航栏是不同的;
- 目前暂时只做了发布公告的功能;
- 注1:本项目采取前后端分离的设计模式,由于经验不足,逻辑可能稍显混乱,但在设计和开发上比较容易增删模块和查找错误原因。前端和后端的交互均通过Ajax实现,用户体验较好;
- 注2:所有页面均设置了防止误操作、直接通过地址访问等非法访问的用户友好界面提示。对于通过直接发送数据包的行为也做了一定的防范。
5.数据库设计
5.1数据库概念结构设计
- 读者实体:用户编号、姓名、性别、年级、专业、被处罚次数、诚信度
- 图书实体:书籍编号、书名、作者、出版社、分类、登记日期
- 用户实体:用户编号、密码
5.2数据库逻辑结构设计
- 读者(读者编号、姓名、性别、年级、专业、被处罚次数、诚信度)
- 图书(图书编号、书名、作者、出版社、分类、登记日期)
- 用户(用户编号、电话号码、邮箱、密码、重置密码)
- 图书借还(用户编号、图书编号、借书时间、预期归还时间、实际归还时间)
- 处罚信息(用户编号、图书编号、超期天数、处罚金额、是否缴纳处罚)
- 管理员(管理员编号、管理员姓名、管理员电话、管理员邮箱、管理员密码)
- 公告(公告编号、公告内容、发布的管理员、发布时间)
5.3数据库物理结构设计
1.用户信息表(User)
|---------------|--------------|------|-----|---------|
| 名字 | 数据类型 | 是否为空 | 键 | 说明 |
| user_ID | int | NO | PRI | 用户编号 |
| stu_number | char(12) | NO | | 学号 |
| user_phone | char(12) | NO | | 用户手机号 |
| user_email | varchar(255) | NO | | 用户邮箱 |
| user_pass | char(50) | NO | | 密码 |
| get_pass_time | datetime | | | 重置密码的时间 |
2.读者信息表(Reader)
|------------|----------|------|-----|-------|
| 名字 | 数据类型 | 是否为空 | 键 | 说明 |
| user_ID | int | NO | PRI | 用户编号 |
| user_name | char(20) | NO | | 用户姓名 |
| user_sex | char(4) | NO | | 性别 |
| user_grade | char(10) | NO | | 年级 |
| user_pro | char(50) | NO | | 专业 |
| pun_time | int | NO | | 被处罚次数 |
| inter | int | NO | | 诚信度 |
3.图书信息表(Book)
|-------------|--------------|------|-----|------|
| 名字 | 数据类型 | 是否为空 | 键 | 说明 |
| book_ID | int | NO | PRI | 图书编号 |
| book_name | varchar(255) | NO | | 书名 |
| book_author | varchar(50) | NO | | 作者 |
| book_pub | varchar(50) | NO | | 出版社 |
| book_cate | varchar(30) | NO | | 分类 |
| book_date | datetime | NO | | 入库时间 |
| book_borrow | tinyint(1) | NO | | 是否借出 |
| book_times | int | NO | | 借出次数 |
4.图书借还信息表(Borrow)
|-----------|------------|------|-----|--------|
| 名字 | 数据类型 | 是否为空 | 键 | 说明 |
| user_ID | int | NO | PRI | 用户编号 |
| book_ID | int | NO | PRI | 图书编号 |
| br_date | datetime | NO | PRI | 借书时间 |
| exp_date | datetime | NO | | 预期归还时间 |
| act_date | datetime | | | 实际归还时间 |
| over_date | int | NO | | 超期天数 |
| penalty | float | NO | | 处罚金额 |
| clear | tinyint(1) | NO | | 是否缴纳处罚 |
5.管理员表(Manager)
|-----------|--------------|------|-----|-------|
| 名字 | 数据类型 | 是否为空 | 键 | 说明 |
| mng_ID | int | NO | PRI | 管理员编号 |
| mng_name | char(20) | NO | | 管理员姓名 |
| mng_email | varchar(255) | NO | | 管理员邮箱 |
| mng_phone | char(12) | NO | | 管理员电话 |
| mng_pass | char(50) | NO | | 管理员密码 |
6.公告表(Public)
|-------------|----------|------|-----|-------|
| 名字 | 数据类型 | 是否为空 | 键 | 说明 |
| pub_ID | int | NO | PRI | 公告编号 |
| pub_content | text | NO | | 公告内容 |
| pub_mng | int | NO | | 发布管理员 |
| pub_time | datetime | NO | | 发布时间 |
6.系统实现
6.1连接数据库
<?php
// 本来只是想存数据库的有关函数,后来索性直接往里面放,文件又写了很多了,就没改名字了
require("../../../../../phpworklib/db.php");
require("../../../../../phpworklib/neteast_mailer.php");
// 查询是否有重复的学号
function is_number_exist($conn, $number) {
$query = "select stu_number from bar_user where stu_number = ?";
return is_xx_exist_s($conn, $number, $query);
}
//查询是否有重复的手机号
function is_phone_exist($conn, $phone) {
$query = "select user_phone from bar_user where user_phone = ?
union select mng_phone from bar_manager where mng_phone = ?";
return is_xx_exist_d($conn, $phone, $query);
}
//查询是否有重复的邮箱
function is_email_exist($conn, $email) {
$query = "select user_email from bar_user where user_email = ?
union select mng_email from bar_manager where mng_phone = ?";
return is_xx_exist_d($conn, $email, $query);
}
//查询重复的通用接口
function is_xx_exist_s($conn, $value, $query) {
$stmt = $conn->prepare($query);
$stmt->bind_param('s', $value);
$stmt->execute();
$stmt->bind_result($result);
$stmt->fetch();
if ($result) {
return true;
} else {
return false;
}
}
function is_xx_exist_d($conn, $value, $query) {
$stmt = $conn->prepare($query);
$stmt->bind_param('ss', $value, $value);
$stmt->execute();
$stmt->bind_result($result);
$stmt->fetch();
if ($result) {
return true;
} else {
return false;
}
}
// 确认有无超期未还书,有就返回TRUE,否则返回FALSE
function check_over_date($conn, $user_ID) {
$today = date('Y-m-d H:i:s');
$query = "select * from bar_borrow where user_ID = $user_ID and exp_date < '$today' and clear = 0;";
$result = $conn->query($query);
if ($result->num_rows > 0) {
return TRUE;
} else {
return FALSE;
}
}
function check_over_number($conn, $user_ID) {
$query = "select * from bar_borrow where user_ID = $user_ID and act_date = '1000-01-01 00:00:00';";
$result = $conn->query($query);
if ($result->num_rows >= 3) {
return TRUE;
} else {
return FALSE;
}
}
function check_over_penalty($conn, $exp_date, $book_ID, $user_ID) {
$today = strtotime(date('Y-m-d H:i:s'));
$exp_date = strtotime($exp_date);
if ($today > $exp_date) {
$over_info = [];
// 超期未还
$diff = $today - $exp_date;
$over_days =abs(round($diff / 86400));
$penalty = $over_days * 0.02;
$query = "update bar_borrow set over_days = $over_days, penalty = $penalty
where book_ID = $book_ID and user_ID = $user_ID and act_date = '1000-01-01 00:00:00';";
$result = $conn->query($query);
if ($result == FALSE) {
// 数据库错误
return -1;
} else {
$over_info['over_days'] = $over_days;
$over_info['penalty'] = $penalty;
return $over_info;
}
} else {
return 0;
}
}
?>
6.2登录逻辑
<?php
session_start();
require("../lib/db_fns.php");
$response = [
'success' => '0',
// 默认-1代表未连接数据库
'code' => '-1',
'msg' => '连接数据库失败,请联系管理员或稍后再试'
];
// $login_id = str_replace(' ','',$_POST['login-id']);
$login_id = $_POST['login-id'];
$password = sha1($_POST['password']);
try {
$conn = db_connect();
} catch (Exception $e) {
echo json_encode($response);
exit();
}
$query = "select user_ID, user_pass from bar_user where user_email = ? or user_phone = ? or stu_number = ?";
$stmt = $conn->prepare($query);
$stmt->bind_param('sss', $login_id, $login_id, $login_id);
$stmt->bind_result($user_ID, $real_pass);
$stmt->execute();
$stmt->fetch();
$stmt->close();
if ($real_pass) {
$isvalid = validate($real_pass, $password, $response);
if ($isvalid) {
// 密码正确,写入$_SESSION['info'];
user_write_to_session($conn, $user_ID);
}
} else {
$query = "select mng_ID, mng_pass from bar_manager where mng_email = ? or mng_phone = ?";
$stmt = $conn->prepare($query);
$stmt->bind_param('ss', $login_id, $login_id);
$stmt->bind_result($mng_ID, $real_pass);
$stmt->execute();
$stmt->fetch();
$stmt->close();
if ($real_pass) {
$isvalid = validate($real_pass, $password, $response);
if ($isvalid) {
$response['t'] = $mng_ID . 'admin';
// 密码正确,写入$_SESSION['info'];
admin_write_to_session($conn, $mng_ID);
}
}
}
echo json_encode($response);
exit();
function validate($real_pass, $password, &$response) {
// 验证密码是否正确的函数
if ($real_pass === $password) {
$response['success'] = '1';
$response['code'] = '0';
$response['msg'] = '登录成功';
return true;
} else {
$response['code'] = '-3';
$response['msg'] = '用户名或密码错误';
return false;
}
}
function user_write_to_session($conn, $user_ID) {
$query = "select user_name from bar_reader where user_ID = ?";
$stmt = $conn->prepare($query);
$stmt->bind_param('i', $user_ID);
$stmt->bind_result($user_name);
$stmt->execute();
$stmt->fetch();
$_SESSION['info'] = $user_name.'|'.$user_ID;
}
function admin_write_to_session($conn, $mng_ID) {
$query = "select mng_name from bar_manager where mng_ID = ?";
$stmt = $conn->prepare($query);
$stmt->bind_param('i', $mng_ID);
$stmt->bind_result($mng_name);
$stmt->execute();
$stmt->fetch();
$_SESSION['info'] = $mng_name.'|'.$mng_ID.'|'.'admin';
}
?>
6.3js代码
$(function () {
'use strict';
if (is_login() == 0) {
var $li = $('<li class="li_book_info"></li>'),
$panel = $('<div class="panel panel-danger"></div>'),
$pheading = $('<div class="panel-heading">您还未登录</div>'),
$pbody = $('<div class="panel-body">请先登录</div>');
$panel.append($pheading);
$panel.append($pbody);
$li.append($panel);
$('#ul-results').append($li);
} else {
$('.info-container').removeClass('hide');
}
prevent_admin();
render_header();
get_account_info();
listen_and_display_change();
listen_change_pass();
function get_account_info() {
$.ajax({
type: "get",
url: "../php/get_account_info.php",
success: function (response) {
response = JSON.parse(response);
if (response.code == '-10') {
alert(response.msg);
window.location.href = '../index.html';
}
if (response.success != '1') {
alert(response.msg);
}
var info = response.account_info;
$('#stu-number').text(info.number);
$('#name').text(info.name);
$('#sex').text(info.sex);
$('#grade').text(info.grade);
$('#pro').text(info.pro);
$('#phone').text(info.phone);
$('#email').text(info.email);
$('#borrow-times').text(info.borrow_time);
$('#over-times').text(info.pun);
$('#inter').text(info.inter);
}
});
}
function display_change_form() {
var $account = $('#account'),
$ch_div = $('#ch-div'),
$div_phone = $(`<div id="div-phone" class="div-info">
<h3>手机号码</h3>
<div class="info input-group-lg">
<input type="text" id="ch-phone" class="form-control">
</div>
<div id="phone-repeat-error" class="error alert alert-danger" role="alert">
<span class="sr-only">Error:</span>
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
手机号码已存在
</div>
</div>`),
$div_email = $(`<div id="div-email" class="div-info">
<h3>电子邮箱</h3>
<div class="info input-group-lg">
<input type="email" id="ch-email" class="form-control">
</div>
<div id="email-repeat-error" class="error alert alert-danger" role="alert">
<span class="sr-only">Error:</span>
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
电子邮箱已存在
</div>
</div>`),
org_phone = $('#phone').text(),
org_email = $('#email').text();
$account.children('.col-xs-4').children().hide();
toggle_btn_account();
$ch_div.append($div_phone);
$('#ch-phone').val(org_phone);
$ch_div.append($div_email);
$('#ch-email').val(org_email);
}
function check_change() {
var $account = $('#account'),
org_phone = $('#phone').text(),
org_email = $('#email').text(),
new_phone = $('#ch-phone').val(),
new_email = $('#ch-email').val(),
$div_phone = $('#div-phone'),
$div_email = $('#div-email');
if (new_phone == org_phone && new_email == org_email) {
$div_phone.remove();
$div_email.remove();
$account.children('.col-xs-4').children().show();
toggle_btn_account();
} else {
go_check(new_phone, new_email);
}
}
function go_check(new_phone, new_email) {
$.ajax({
type: "post",
url: "../php/check_change.php",
data: {
'new_phone': new_phone,
'new_email': new_email,
},
success: function (response) {
response = JSON.parse(response);
if (response.success == '1') {
alert(response.msg);
window.location.reload();
} else if (response.code == '2') {
$('#phone-repeat-error').show();
$('#ch-phone').trigger('select');
} else if (response.code == '3') {
$('#email-repeat-error').show();
$('#ch-email').trigger('select');
} else {
$('#phone-repeat-error').show();
$('#email-repeat-error').show();
$('#ch-phone').trigger('select');
}
}
});
}
function toggle_btn_account() {
var $btn_account = $('#ch-account');
if ($btn_account.hasClass('btn-primary')) {
$btn_account.removeClass('btn-primary');
$btn_account.text('编辑');
} else {
$btn_account.addClass('btn-primary');
$btn_account.text('完成');
}
$btn_account.trigger('blur');
}
function toggle_pass_input() {
var $input = $('#pass-input-div'),
$button = $('#pass-button');
if ($input.hasClass('hide')) {
$input.removeClass('hide');
$button.removeClass('hide');
} else {
$input.addClass('hide');
$button.addClass('hide');
}
}
function listen_and_display_change() {
$('#ch-account').on('click', function() {
if ($(this).hasClass('btn-primary')) {
check_change();
} else {
display_change_form();
}
});
$('#ch-pass').on('click', function() {
$('#pass-info').hide();
toggle_pass_input();
});
}
function listen_change_pass() {
$('#pass-input').on('change', function() {
var $confirm = $('#pass-confirm');
if ($confirm.val() != '') {
check_pass_same();
}
});
$('#pass-confirm').on('change', function() {
check_pass_same();
});
$('#cancle').on('click', function() {
$('#pass-info').show();
toggle_pass_input();
});
$('#confirm').on('click', function() {
var newpass = $('#pass-input').val(),
confirm = $('#pass-confirm').val();
if (newpass !== confirm) {
$('#not-same').show();
return;
}
$.ajax({
type: "post",
url: "../php/change_password.php",
data: {
'newpass': newpass,
},
success: function (response) {
response = JSON.parse(response);
console.log(response);
alert(response.msg);
if (response.success == '1') {
window.location.href = "../account/login.html";
}
}
});
});
}
function check_pass_same() {
var newpass = $('#pass-input').val(),
confirm = $('#pass-confirm').val();
if (newpass !== confirm) {
$('#not-same').show();
} else {
$('#not-same').hide();
}
if (newpass == '' && confirm == '') {
$('#not-same').hide();
}
}
})
6.4视图代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>个人信息</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="../css/common.css">
<link rel="stylesheet" href="../css/account.css">
</head>
<body>
<div id="header">
<!-- 先判断是否登录,再添加页头 -->
</div>
<div class="title">
<div class="bg"></div>
<h1>个人信息</h1>
</div>
<div class="col-xs-12">
<div id="div-results">
<ul id="ul-results" class="list-group row">
</ul>
</div>
</div>
<div id="account" class="container info-container hide">
<div class="col-xs-3">
<h1 class="subtitle">账户</h1>
</div>
<div id="ch-div" class="col-xs-4">
<div class="div-info">
<h3>学号</h3>
<div id="stu-number" class="info"></div>
</div>
<div class="div-info">
<h3>姓名</h3>
<div id="name" class="info"></div>
</div>
<div class="div-info">
<h3>性别</h3>
<div id="sex" class="info"></div>
</div>
</div>
<div class="col-xs-4">
<div class="div-info">
<h3>年级</h3>
<div id="grade" class="info"></div>
</div>
<div class="div-info">
<h3>专业</h3>
<div id="pro" class="info"></div>
</div>
<div class="div-info">
<h3>联络方式</h3>
<div id="contact" class="info">
<div id="phone"></div>
<div id="email"></div>
</div>
</div>
</div>
<div class="col-xs-1">
<button type="button" id="ch-account" class="btn btn-default">编辑</button>
</div>
</div>
<div id="security" class="container info-container hide">
<div class="col-xs-3">
<h1 class="subtitle">安全</h1>
</div>
<div class="col-xs-4">
<div id="pass-info" class="div-info">
<h3>密码</h3>
<div>
<button type="button" id="ch-pass" class="btn btn-link">更改密码</button>
</div>
</div>
<div id="pass-input-div" class="div-info hide">
<div class="form-group">
<input type="password" id="pass-input" class="form-control" placeholder="请输入新密码">
</div>
<div class="form-group">
<input type="password" id="pass-confirm" class="form-control" placeholder="请确认新密码">
<div id="not-same" class="error alert alert-danger" role="alert">
<span class="sr-only">Error:</span>
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
两次密码输入不一致
</div>
</div>
</div>
</div>
<div class="col-xs-4">
<div id="pass-button" class="div-info hide">
<div class="form-group">
<button type="button" id="confirm" class="btn btn-primary">确认</button>
</div>
<div class="form-group">
<button type="button" id="cancle" class="btn btn-default">取消</button>
</div>
</div>
</div>
<div class="col-xs-1"></div>
</div>
<div id="b-and-r" class="container info-container hide no-border">
<div class="col-xs-3">
<h1 class="subtitle">借阅</h1>
</div>
<div class="col-xs-4">
<div class="div-info">
<h3>借书次数</h3>
<div id="borrow-times" class="info"></div>
</div>
<div class="div-info">
<h3>超期次数</h3>
<div id="over-times" class="info"></div>
</div>
</div>
<div class="col-xs-4">
<div class="div-info">
<h3>信用等级</h3>
<div id="inter" class="info"></div>
</div>
</div>
<div class="col-xs-1"></div>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.js"></script>
<script src="../js/common_fns.js"></script>
<script src="../js/vheader.js"></script>
<script src="../js/account.js"></script>
</body>
</html>