login_register_dashboard

This commit is contained in:
superlishunqin 2025-04-29 11:18:18 +08:00
parent bfe6ec8c6d
commit 568d46f013
49 changed files with 5437 additions and 0 deletions

64
all_file_output.py Normal file
View File

@ -0,0 +1,64 @@
import os
import sys
def collect_code_files(output_file="code_collection.txt"):
# 定义代码文件扩展名
code_extensions = [
'.py', '.java', '.cpp', '.c', '.h', '.hpp', '.cs',
'.js', '.html', '.css', '.php', '.go', '.rb',
'.swift', '.kt', '.ts', '.sh', '.pl', '.r'
]
# 定义要排除的目录
excluded_dirs = [
'venv', 'env', '.venv', '.env', 'virtualenv',
'__pycache__', 'node_modules', '.git', '.idea',
'dist', 'build', 'target', 'bin'
]
# 计数器
file_count = 0
# 打开输出文件
with open(output_file, 'w', encoding='utf-8') as out_file:
# 遍历当前目录及所有子目录
for root, dirs, files in os.walk('.'):
# 从dirs中移除排除的目录这会阻止os.walk进入这些目录
dirs[:] = [d for d in dirs if d not in excluded_dirs]
for file in files:
# 获取文件扩展名
_, ext = os.path.splitext(file)
# 检查是否为代码文件
if ext.lower() in code_extensions:
file_path = os.path.join(root, file)
file_count += 1
# 写入文件路径作为分隔
out_file.write(f"\n{'=' * 80}\n")
out_file.write(f"File: {file_path}\n")
out_file.write(f"{'=' * 80}\n\n")
# 尝试读取文件内容并写入
try:
with open(file_path, 'r', encoding='utf-8') as code_file:
out_file.write(code_file.read())
except UnicodeDecodeError:
# 尝试用不同的编码
try:
with open(file_path, 'r', encoding='latin-1') as code_file:
out_file.write(code_file.read())
except Exception as e:
out_file.write(f"无法读取文件内容: {str(e)}\n")
except Exception as e:
out_file.write(f"读取文件时出错: {str(e)}\n")
print(f"已成功收集 {file_count} 个代码文件到 {output_file}")
if __name__ == "__main__":
# 如果提供了命令行参数,则使用它作为输出文件名
output_file = sys.argv[1] if len(sys.argv) > 1 else "code_collection.txt"
collect_code_files(output_file)

6
app.py Normal file
View File

@ -0,0 +1,6 @@
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=49666)

85
app/__init__.py Normal file
View File

@ -0,0 +1,85 @@
from flask import Flask, render_template, session, g
from app.models.user import db, User
from app.controllers.user import user_bp
import os
def create_app():
app = Flask(__name__)
# 配置应用
app.config.from_mapping(
SECRET_KEY=os.environ.get('SECRET_KEY', 'dev_key_replace_in_production'),
SQLALCHEMY_DATABASE_URI='mysql+pymysql://book20250428:booksystem@27.124.22.104/book_system',
SQLALCHEMY_TRACK_MODIFICATIONS=False,
PERMANENT_SESSION_LIFETIME=86400 * 7, # 7天
# 邮件配置
EMAIL_HOST='smtp.qq.com',
EMAIL_PORT=587,
EMAIL_ENCRYPTION='starttls',
EMAIL_USERNAME='3399560459@qq.com',
EMAIL_PASSWORD='fzwhyirhbqdzcjgf', # 这是你的SMTP授权码不是邮箱密码
EMAIL_FROM='3399560459@qq.com',
EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL'
)
# 实例配置,如果存在
app.config.from_pyfile('config.py', silent=True)
# 初始化数据库
db.init_app(app)
# 注册蓝图
app.register_blueprint(user_bp, url_prefix='/user')
# 创建数据库表
with app.app_context():
db.create_all()
# 创建默认角色
from app.models.user import Role
if not Role.query.filter_by(id=1).first():
admin_role = Role(id=1, role_name='管理员', description='系统管理员')
db.session.add(admin_role)
if not Role.query.filter_by(id=2).first():
user_role = Role(id=2, role_name='普通用户', description='普通用户')
db.session.add(user_role)
# 创建管理员账号
if not User.query.filter_by(username='admin').first():
admin = User(
username='admin',
password='admin123',
email='admin@example.com',
role_id=1,
nickname='系统管理员'
)
db.session.add(admin)
db.session.commit()
# 请求前处理
@app.before_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = User.query.get(user_id)
# 首页路由
@app.route('/')
def index():
if not g.user:
return render_template('login.html')
return render_template('index.html', current_user=g.user)
# 错误处理
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
return app

View File

View File

0
app/controllers/book.py Normal file
View File

View File

View File

0
app/controllers/log.py Normal file
View File

View File

181
app/controllers/user.py Normal file
View File

@ -0,0 +1,181 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify
from werkzeug.security import generate_password_hash, check_password_hash
from app.models.user import User, db
from app.utils.email import send_verification_email, generate_verification_code
import logging
from functools import wraps
import time
from datetime import datetime, timedelta
# 创建蓝图
user_bp = Blueprint('user', __name__)
# 使用内存字典代替Redis存储验证码
class VerificationStore:
def __init__(self):
self.codes = {} # 存储格式: {email: {'code': code, 'expires': timestamp}}
def setex(self, email, seconds, code):
"""设置验证码并指定过期时间"""
expiry = datetime.now() + timedelta(seconds=seconds)
self.codes[email] = {'code': code, 'expires': expiry}
return True
def get(self, email):
"""获取验证码如果过期则返回None"""
if email not in self.codes:
return None
data = self.codes[email]
if datetime.now() > data['expires']:
# 验证码已过期,删除它
self.delete(email)
return None
return data['code']
def delete(self, email):
"""删除验证码"""
if email in self.codes:
del self.codes[email]
return True
# 使用内存存储验证码
verification_codes = VerificationStore()
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('user.login'))
return f(*args, **kwargs)
return decorated_function
@user_bp.route('/login', methods=['GET', 'POST'])
def login():
# 保持原代码不变
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
remember_me = request.form.get('remember_me') == 'on'
if not username or not password:
return render_template('login.html', error='用户名和密码不能为空')
# 检查用户是否存在
user = User.query.filter((User.username == username) | (User.email == username)).first()
if not user or not user.check_password(password):
return render_template('login.html', error='用户名或密码错误')
if user.status == 0:
return render_template('login.html', error='账号已被禁用,请联系管理员')
# 登录成功,保存用户信息到会话
session['user_id'] = user.id
session['username'] = user.username
session['role_id'] = user.role_id
if remember_me:
# 设置会话过期时间为7天
session.permanent = True
# 记录登录日志(可选)
# log_user_action('用户登录')
# 重定向到首页
return redirect(url_for('index'))
return render_template('login.html')
@user_bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
verification_code = request.form.get('verification_code')
# 验证表单数据
if not username or not email or not password or not confirm_password or not verification_code:
return render_template('register.html', error='所有字段都是必填项')
if password != confirm_password:
return render_template('register.html', error='两次输入的密码不匹配')
# 检查用户名和邮箱是否已存在
if User.query.filter_by(username=username).first():
return render_template('register.html', error='用户名已存在')
if User.query.filter_by(email=email).first():
return render_template('register.html', error='邮箱已被注册')
# 验证验证码
stored_code = verification_codes.get(email)
if not stored_code or stored_code != verification_code:
return render_template('register.html', error='验证码无效或已过期')
# 创建新用户
try:
new_user = User(
username=username,
password=password, # 密码会在模型中自动哈希
email=email,
nickname=username # 默认昵称与用户名相同
)
db.session.add(new_user)
db.session.commit()
# 清除验证码
verification_codes.delete(email)
flash('注册成功,请登录', 'success')
return redirect(url_for('user.login'))
except Exception as e:
db.session.rollback()
logging.error(f"User registration failed: {str(e)}")
return render_template('register.html', error='注册失败,请稍后重试')
return render_template('register.html')
@user_bp.route('/logout')
def logout():
# 清除会话数据
session.pop('user_id', None)
session.pop('username', None)
session.pop('role_id', None)
return redirect(url_for('user.login'))
@user_bp.route('/send_verification_code', methods=['POST'])
def send_verification_code():
data = request.get_json()
email = data.get('email')
if not email:
return jsonify({'success': False, 'message': '请提供邮箱地址'})
# 检查邮箱格式
import re
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
return jsonify({'success': False, 'message': '邮箱格式不正确'})
# 生成验证码
code = generate_verification_code()
# 存储验证码(10分钟有效)
verification_codes.setex(email, 600, code) # 10分钟过期
# 发送验证码邮件
if send_verification_email(email, code):
return jsonify({'success': True, 'message': '验证码已发送'})
else:
return jsonify({'success': False, 'message': '邮件发送失败,请稍后重试'})

0
app/models/__init__.py Normal file
View File

View File

0
app/models/book.py Normal file
View File

0
app/models/borrow.py Normal file
View File

0
app/models/inventory.py Normal file
View File

0
app/models/log.py Normal file
View File

View File

75
app/models/user.py Normal file
View File

@ -0,0 +1,75 @@
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
db = SQLAlchemy()
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(64), unique=True, nullable=False)
password = db.Column(db.String(255), nullable=False)
email = db.Column(db.String(128), unique=True, nullable=True)
phone = db.Column(db.String(20), unique=True, nullable=True)
nickname = db.Column(db.String(64), nullable=True)
status = db.Column(db.Integer, default=1) # 1: active, 0: disabled
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), default=2) # 2: 普通用户, 1: 管理员
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
def __init__(self, username, password, email=None, phone=None, nickname=None, role_id=2):
self.username = username
self.set_password(password)
self.email = email
self.phone = phone
self.nickname = nickname
self.role_id = role_id
def set_password(self, password):
"""设置密码,使用哈希加密"""
self.password = generate_password_hash(password)
def check_password(self, password):
"""验证密码"""
return check_password_hash(self.password, password)
def to_dict(self):
"""转换为字典格式"""
return {
'id': self.id,
'username': self.username,
'email': self.email,
'phone': self.phone,
'nickname': self.nickname,
'status': self.status,
'role_id': self.role_id,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S')
}
@classmethod
def create_user(cls, username, password, email=None, phone=None, nickname=None, role_id=2):
"""创建新用户"""
user = User(
username=username,
password=password,
email=email,
phone=phone,
nickname=nickname,
role_id=role_id
)
db.session.add(user)
db.session.commit()
return user
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
role_name = db.Column(db.String(32), unique=True, nullable=False)
description = db.Column(db.String(128))
users = db.relationship('User', backref='role')

0
app/services/__init__.py Normal file
View File

View File

View File

View File

View File

651
app/static/css/index.css Normal file
View File

@ -0,0 +1,651 @@
/* index.css - 仅用于图书管理系统首页/仪表板 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
font-size: 16px;
line-height: 1.6;
}
a {
text-decoration: none;
color: #4a89dc;
}
ul {
list-style: none;
}
/* 应用容器 */
.app-container {
display: flex;
min-height: 100vh;
}
/* 侧边导航栏 */
.sidebar {
width: 250px;
background-color: #2c3e50;
color: #ecf0f1;
padding: 20px 0;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
position: fixed;
height: 100vh;
overflow-y: auto;
}
.logo-container {
padding: 0 20px 20px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 20px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.logo {
width: 60px;
height: auto;
margin-bottom: 10px;
}
.logo-container h2 {
font-size: 1.2rem;
margin: 10px 0;
color: #ecf0f1;
font-weight: 500;
}
.nav-links li {
margin-bottom: 5px;
}
.nav-links li a {
padding: 10px 20px;
display: flex;
align-items: center;
color: #bdc3c7;
transition: all 0.3s ease;
}
.nav-links li a i {
margin-right: 10px;
font-size: 1.1rem;
width: 20px;
text-align: center;
}
.nav-links li a:hover, .nav-links li.active a {
background-color: #34495e;
color: #ecf0f1;
border-left: 3px solid #4a89dc;
}
.nav-category {
padding: 10px 20px;
font-size: 0.85rem;
text-transform: uppercase;
color: #7f8c8d;
margin-top: 15px;
margin-bottom: 5px;
}
/* 主内容区 */
.main-content {
flex: 1;
margin-left: 250px;
padding: 20px;
}
/* 顶部导航栏 */
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 30px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
margin-bottom: 20px;
}
.search-container {
position: relative;
width: 300px;
}
.search-input {
padding: 10px 15px 10px 40px;
width: 100%;
border: 1px solid #e1e4e8;
border-radius: 20px;
font-size: 14px;
transition: all 0.3s ease;
}
.search-input:focus {
border-color: #4a89dc;
box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.1);
outline: none;
}
.search-icon {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: #8492a6;
}
.user-menu {
display: flex;
align-items: center;
}
.notifications {
margin-right: 20px;
position: relative;
cursor: pointer;
}
.notifications i {
font-size: 1.2rem;
color: #606266;
}
.badge {
position: absolute;
top: -8px;
right: -8px;
background-color: #f56c6c;
color: white;
font-size: 0.7rem;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.user-info {
display: flex;
align-items: center;
position: relative;
cursor: pointer;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #4a89dc;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
font-size: 1.2rem;
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-weight: 500;
color: #333;
}
.user-role {
font-size: 0.8rem;
color: #8492a6;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
padding: 10px 0;
min-width: 150px;
display: none;
z-index: 10;
}
.user-info.active .dropdown-menu {
display: block;
}
.dropdown-menu a {
display: block;
padding: 8px 15px;
color: #606266;
transition: all 0.3s ease;
}
.dropdown-menu a:hover {
background-color: #f5f7fa;
}
.dropdown-menu a i {
margin-right: 8px;
width: 16px;
text-align: center;
}
/* 欢迎区域 */
.welcome-section {
background: linear-gradient(to right, #4a89dc, #5d9cec);
color: white;
padding: 30px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
}
.welcome-section h1 {
font-size: 1.8rem;
margin-bottom: 5px;
}
.welcome-section p {
font-size: 1rem;
opacity: 0.9;
}
/* 统计卡片样式 */
.stats-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
padding: 20px;
display: flex;
align-items: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.stat-icon {
font-size: 2rem;
color: #4a89dc;
margin-right: 15px;
width: 40px;
text-align: center;
}
.stat-info h3 {
font-size: 0.9rem;
color: #606266;
margin-bottom: 5px;
}
.stat-number {
font-size: 1.8rem;
font-weight: 600;
color: #2c3e50;
}
/* 主要内容区域 */
.main-sections {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.content-section {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
padding: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #edf2f7;
}
.section-header h2 {
font-size: 1.2rem;
color: #2c3e50;
}
.view-all {
font-size: 0.85rem;
color: #4a89dc;
display: flex;
align-items: center;
}
.view-all i {
margin-left: 5px;
transition: transform 0.3s ease;
}
.view-all:hover i {
transform: translateX(3px);
}
/* 图书卡片样式 */
.book-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.book-card {
display: flex;
border: 1px solid #edf2f7;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.book-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
}
.book-cover {
width: 100px;
height: 140px;
min-width: 100px;
background-color: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.book-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.book-info {
padding: 15px;
flex: 1;
display: flex;
flex-direction: column;
}
.book-title {
font-size: 1rem;
margin-bottom: 5px;
color: #2c3e50;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.book-author {
font-size: 0.85rem;
color: #606266;
margin-bottom: 10px;
}
.book-meta {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.book-category {
background-color: #e5f1ff;
color: #4a89dc;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
}
.book-status {
font-size: 0.75rem;
font-weight: 500;
}
.book-status.available {
color: #67c23a;
}
.book-status.borrowed {
color: #e6a23c;
}
.borrow-btn {
background-color: #4a89dc;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
margin-top: auto;
transition: background-color 0.3s ease;
}
.borrow-btn:hover {
background-color: #357bc8;
}
/* 通知公告样式 */
.notice-item {
display: flex;
padding: 15px 0;
border-bottom: 1px solid #edf2f7;
}
.notice-item:last-child {
border-bottom: none;
}
.notice-icon {
font-size: 1.5rem;
color: #4a89dc;
margin-right: 15px;
display: flex;
align-items: flex-start;
padding-top: 5px;
}
.notice-content h3 {
font-size: 1rem;
color: #2c3e50;
margin-bottom: 5px;
}
.notice-content p {
font-size: 0.9rem;
color: #606266;
margin-bottom: 10px;
}
.notice-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.notice-time {
font-size: 0.8rem;
color: #8492a6;
}
.renew-btn {
background-color: #ecf5ff;
color: #4a89dc;
border: 1px solid #d9ecff;
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
}
.renew-btn:hover {
background-color: #4a89dc;
color: white;
border-color: #4a89dc;
}
/* 热门图书区域 */
.popular-section {
margin-top: 20px;
}
.popular-books {
display: flex;
overflow-x: auto;
gap: 15px;
padding-bottom: 10px;
}
.popular-book-item {
display: flex;
background-color: #f8fafc;
border-radius: 8px;
padding: 15px;
min-width: 280px;
position: relative;
}
.rank-badge {
position: absolute;
top: -10px;
left: 10px;
background-color: #4a89dc;
color: white;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 0.8rem;
font-weight: bold;
}
.book-cover.small {
width: 60px;
height: 90px;
min-width: 60px;
margin-right: 15px;
}
.book-details {
flex: 1;
}
.book-stats {
display: flex;
flex-direction: column;
gap: 5px;
margin-top: 10px;
}
.book-stats span {
font-size: 0.8rem;
color: #8492a6;
}
.book-stats i {
margin-right: 5px;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.stats-container {
grid-template-columns: repeat(2, 1fr);
}
.main-sections {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.sidebar {
width: 70px;
overflow: hidden;
}
.logo-container {
padding: 10px;
}
.logo-container h2 {
display: none;
}
.nav-links li a span {
display: none;
}
.nav-links li a i {
margin-right: 0;
}
.nav-category {
display: none;
}
.main-content {
margin-left: 70px;
}
.search-container {
width: 180px;
}
.book-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 576px) {
.stats-container {
grid-template-columns: 1fr;
}
.top-bar {
flex-direction: column;
gap: 15px;
}
.search-container {
width: 100%;
}
.user-details {
display: none;
}
}

469
app/static/css/main.css Normal file
View File

@ -0,0 +1,469 @@
/* 主样式文件 - 从登录页面复制过来的样式 */
/* 从您提供的登录页CSS复制但省略了不需要的部分 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
:root {
--primary-color: #4a89dc;
--primary-hover: #3b78c4;
--secondary-color: #5cb85c;
--text-color: #333;
--light-text: #666;
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--border-color: #ddd;
--error-color: #e74c3c;
--success-color: #2ecc71;
}
body.dark-mode {
--primary-color: #5a9aed;
--primary-hover: #4a89dc;
--secondary-color: #6bc76b;
--text-color: #f1f1f1;
--light-text: #aaa;
--bg-color: #1a1a1a;
--card-bg: #2c2c2c;
--border-color: #444;
}
body {
background-color: var(--bg-color);
background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
background-size: cover;
background-position: center;
display: flex;
flex-direction: column;
min-height: 100vh;
color: var(--text-color);
transition: all 0.3s ease;
}
.theme-toggle {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
cursor: pointer;
padding: 8px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.overlay {
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(5px);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
}
.main-container {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
padding: 20px;
}
.login-container {
background-color: var(--card-bg);
border-radius: 12px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
width: 450px;
padding: 35px;
position: relative;
overflow: hidden;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.logo {
text-align: center;
margin-bottom: 25px;
position: relative;
}
.logo img {
width: 90px;
height: 90px;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 5px;
background-color: #fff;
transition: transform 0.3s ease;
}
h1 {
text-align: center;
color: var(--text-color);
margin-bottom: 10px;
font-weight: 600;
font-size: 28px;
}
.subtitle {
text-align: center;
color: var(--light-text);
margin-bottom: 30px;
font-size: 14px;
}
.form-group {
margin-bottom: 22px;
position: relative;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--text-color);
font-weight: 500;
font-size: 14px;
}
.input-with-icon {
position: relative;
}
.input-icon {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: var(--light-text);
}
.form-control {
width: 100%;
height: 48px;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 0 15px 0 45px;
font-size: 15px;
transition: all 0.3s ease;
background-color: var(--card-bg);
color: var(--text-color);
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.2);
outline: none;
}
.password-toggle {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: var(--light-text);
}
.validation-message {
margin-top: 6px;
font-size: 12px;
color: var(--error-color);
display: none;
}
.validation-message.show {
display: block;
animation: shake 0.5s ease;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.remember-forgot {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
}
.custom-checkbox {
position: relative;
padding-left: 30px;
cursor: pointer;
font-size: 14px;
user-select: none;
color: var(--light-text);
}
.custom-checkbox input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 18px;
width: 18px;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 3px;
transition: all 0.2s ease;
}
.custom-checkbox:hover input ~ .checkmark {
border-color: var(--primary-color);
}
.custom-checkbox input:checked ~ .checkmark {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.checkmark:after {
content: "";
position: absolute;
display: none;
}
.custom-checkbox input:checked ~ .checkmark:after {
display: block;
}
.custom-checkbox .checkmark:after {
left: 6px;
top: 2px;
width: 4px;
height: 9px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.forgot-password a {
color: var(--primary-color);
text-decoration: none;
font-size: 14px;
transition: color 0.3s ease;
}
.forgot-password a:hover {
color: var(--primary-hover);
text-decoration: underline;
}
.btn-login {
width: 100%;
height: 48px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btn-login:hover {
background-color: var(--primary-hover);
}
.btn-login:active {
transform: scale(0.98);
}
.btn-login .loading {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.btn-login.loading-state {
color: transparent;
}
.btn-login.loading-state .loading {
display: block;
}
.signup {
text-align: center;
margin-top: 25px;
font-size: 14px;
color: var(--light-text);
}
.signup a {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
transition: color 0.3s ease;
}
.signup a:hover {
color: var(--primary-hover);
text-decoration: underline;
}
.features {
display: flex;
justify-content: center;
margin-top: 25px;
gap: 30px;
}
.feature-item {
text-align: center;
font-size: 12px;
color: var(--light-text);
display: flex;
flex-direction: column;
align-items: center;
}
.feature-icon {
margin-bottom: 5px;
font-size: 18px;
}
footer {
text-align: center;
padding: 20px;
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
}
footer a {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
}
.alert {
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
color: #721c24;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
}
.verification-code-container {
display: flex;
gap: 10px;
}
.verification-input {
flex: 1;
height: 48px;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 0 15px;
font-size: 15px;
}
.send-code-btn {
padding: 0 15px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
white-space: nowrap;
}
.register-container {
width: 500px;
}
@media (max-width: 576px) {
.login-container {
width: 100%;
padding: 25px;
border-radius: 0;
}
.theme-toggle {
top: 10px;
}
.logo img {
width: 70px;
height: 70px;
}
h1 {
font-size: 22px;
}
.main-container {
padding: 0;
}
.verification-code-container {
flex-direction: column;
}
.register-container {
width: 100%;
}
}
.verification-code-container {
display: flex;
gap: 10px;
}
.verification-input {
flex: 1;
height: 48px;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 0 15px;
font-size: 15px;
transition: all 0.3s ease;
background-color: var(--card-bg);
color: var(--text-color);
}
.send-code-btn {
padding: 0 15px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
white-space: nowrap;
transition: all 0.3s ease;
}
.send-code-btn:hover {
background-color: var(--primary-hover);
}
.send-code-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}

BIN
app/static/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 KiB

340
app/static/js/main.js Normal file
View File

@ -0,0 +1,340 @@
// 主JS文件 - 包含登录和注册功能
document.addEventListener('DOMContentLoaded', function() {
// 主题切换
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
const body = document.body;
themeToggle.addEventListener('click', function() {
body.classList.toggle('dark-mode');
const isDarkMode = body.classList.contains('dark-mode');
localStorage.setItem('dark-mode', isDarkMode);
themeToggle.innerHTML = isDarkMode ? '🌙' : '☀️';
});
// 从本地存储中加载主题首选项
const savedDarkMode = localStorage.getItem('dark-mode') === 'true';
if (savedDarkMode) {
body.classList.add('dark-mode');
themeToggle.innerHTML = '🌙';
}
}
// 密码可见性切换
const passwordToggle = document.getElementById('password-toggle');
if (passwordToggle) {
const passwordInput = document.getElementById('password');
passwordToggle.addEventListener('click', function() {
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
passwordInput.setAttribute('type', type);
passwordToggle.innerHTML = type === 'password' ? '👁️' : '👁️‍🗨️';
});
}
// 登录表单验证
const loginForm = document.getElementById('login-form');
if (loginForm) {
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const usernameError = document.getElementById('username-error');
const passwordError = document.getElementById('password-error');
const loginButton = document.getElementById('login-button');
if (usernameInput && usernameError) {
usernameInput.addEventListener('input', function() {
if (usernameInput.value.trim() === '') {
usernameError.textContent = '用户名不能为空';
usernameError.classList.add('show');
} else {
usernameError.classList.remove('show');
}
});
}
if (passwordInput && passwordError) {
passwordInput.addEventListener('input', function() {
if (passwordInput.value.trim() === '') {
passwordError.textContent = '密码不能为空';
passwordError.classList.add('show');
} else if (passwordInput.value.length < 6) {
passwordError.textContent = '密码长度至少6位';
passwordError.classList.add('show');
} else {
passwordError.classList.remove('show');
}
});
}
loginForm.addEventListener('submit', function(e) {
let isValid = true;
// 验证用户名
if (usernameInput.value.trim() === '') {
usernameError.textContent = '用户名不能为空';
usernameError.classList.add('show');
isValid = false;
}
// 验证密码
if (passwordInput.value.trim() === '') {
passwordError.textContent = '密码不能为空';
passwordError.classList.add('show');
isValid = false;
} else if (passwordInput.value.length < 6) {
passwordError.textContent = '密码长度至少6位';
passwordError.classList.add('show');
isValid = false;
}
if (!isValid) {
e.preventDefault();
} else if (loginButton) {
loginButton.classList.add('loading-state');
}
});
}
// 注册表单验证
const registerForm = document.getElementById('register-form');
if (registerForm) {
const usernameInput = document.getElementById('username');
const emailInput = document.getElementById('email');
const passwordInput = document.getElementById('password');
const confirmPasswordInput = document.getElementById('confirm_password');
const verificationCodeInput = document.getElementById('verification_code');
const usernameError = document.getElementById('username-error');
const emailError = document.getElementById('email-error');
const passwordError = document.getElementById('password-error');
const confirmPasswordError = document.getElementById('confirm-password-error');
const verificationCodeError = document.getElementById('verification-code-error');
const registerButton = document.getElementById('register-button');
const sendCodeBtn = document.getElementById('send-code-btn');
// 用户名验证
if (usernameInput && usernameError) {
usernameInput.addEventListener('input', function() {
if (usernameInput.value.trim() === '') {
usernameError.textContent = '用户名不能为空';
usernameError.classList.add('show');
} else if (usernameInput.value.length < 3) {
usernameError.textContent = '用户名至少3个字符';
usernameError.classList.add('show');
} else {
usernameError.classList.remove('show');
}
});
}
// 邮箱验证
if (emailInput && emailError) {
emailInput.addEventListener('input', function() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailInput.value.trim() === '') {
emailError.textContent = '邮箱不能为空';
emailError.classList.add('show');
} else if (!emailRegex.test(emailInput.value)) {
emailError.textContent = '请输入有效的邮箱地址';
emailError.classList.add('show');
} else {
emailError.classList.remove('show');
}
});
}
// 密码验证
if (passwordInput && passwordError) {
passwordInput.addEventListener('input', function() {
if (passwordInput.value.trim() === '') {
passwordError.textContent = '密码不能为空';
passwordError.classList.add('show');
} else if (passwordInput.value.length < 6) {
passwordError.textContent = '密码长度至少6位';
passwordError.classList.add('show');
} else {
passwordError.classList.remove('show');
}
// 检查确认密码是否匹配
if (confirmPasswordInput && confirmPasswordInput.value) {
if (confirmPasswordInput.value !== passwordInput.value) {
confirmPasswordError.textContent = '两次输入的密码不匹配';
confirmPasswordError.classList.add('show');
} else {
confirmPasswordError.classList.remove('show');
}
}
});
}
// 确认密码验证
if (confirmPasswordInput && confirmPasswordError) {
confirmPasswordInput.addEventListener('input', function() {
if (confirmPasswordInput.value.trim() === '') {
confirmPasswordError.textContent = '请确认密码';
confirmPasswordError.classList.add('show');
} else if (confirmPasswordInput.value !== passwordInput.value) {
confirmPasswordError.textContent = '两次输入的密码不匹配';
confirmPasswordError.classList.add('show');
} else {
confirmPasswordError.classList.remove('show');
}
});
}
// 发送验证码按钮
if (sendCodeBtn) {
sendCodeBtn.addEventListener('click', function() {
const email = emailInput.value.trim();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email) {
emailError.textContent = '请输入邮箱地址';
emailError.classList.add('show');
return;
} else if (!emailRegex.test(email)) {
emailError.textContent = '请输入有效的邮箱地址';
emailError.classList.add('show');
return;
}
// 禁用按钮并显示倒计时
let countdown = 60;
sendCodeBtn.disabled = true;
const originalText = sendCodeBtn.textContent;
sendCodeBtn.textContent = `${countdown}秒后重试`;
const timer = setInterval(() => {
countdown--;
sendCodeBtn.textContent = `${countdown}秒后重试`;
if (countdown <= 0) {
clearInterval(timer);
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = originalText;
}
}, 1000);
// 发送请求获取验证码
fetch('/user/send_verification_code', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: email }),
})
.then(response => response.json())
.then(data => {
console.log("验证码发送响应:", data); // 添加调试日志
if (data.success) {
showMessage('验证码已发送', '请检查您的邮箱', 'success');
} else {
showMessage('发送失败', data.message || '请稍后重试', 'error');
clearInterval(timer);
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = originalText;
}
})
.catch(error => {
console.error('Error:', error);
showMessage('发送失败', '网络错误,请稍后重试', 'error');
clearInterval(timer);
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = originalText;
});
});
}
// 表单提交验证
registerForm.addEventListener('submit', function(e) {
let isValid = true;
// 验证用户名
if (usernameInput.value.trim() === '') {
usernameError.textContent = '用户名不能为空';
usernameError.classList.add('show');
isValid = false;
} else if (usernameInput.value.length < 3) {
usernameError.textContent = '用户名至少3个字符';
usernameError.classList.add('show');
isValid = false;
}
// 验证邮箱
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailInput.value.trim() === '') {
emailError.textContent = '邮箱不能为空';
emailError.classList.add('show');
isValid = false;
} else if (!emailRegex.test(emailInput.value)) {
emailError.textContent = '请输入有效的邮箱地址';
emailError.classList.add('show');
isValid = false;
}
// 验证密码
if (passwordInput.value.trim() === '') {
passwordError.textContent = '密码不能为空';
passwordError.classList.add('show');
isValid = false;
} else if (passwordInput.value.length < 6) {
passwordError.textContent = '密码长度至少6位';
passwordError.classList.add('show');
isValid = false;
}
// 验证确认密码
if (confirmPasswordInput.value.trim() === '') {
confirmPasswordError.textContent = '请确认密码';
confirmPasswordError.classList.add('show');
isValid = false;
} else if (confirmPasswordInput.value !== passwordInput.value) {
confirmPasswordError.textContent = '两次输入的密码不匹配';
confirmPasswordError.classList.add('show');
isValid = false;
}
// 验证验证码
if (verificationCodeInput.value.trim() === '') {
verificationCodeError.textContent = '请输入验证码';
verificationCodeError.classList.add('show');
isValid = false;
}
if (!isValid) {
e.preventDefault();
} else if (registerButton) {
registerButton.classList.add('loading-state');
}
});
}
// 通知消息显示函数
function showMessage(title, message, type) {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
const icon = type === 'success' ? '✓' : '✗';
notification.innerHTML = `
<div class="notification-icon">${icon}</div>
<div class="notification-content">
<h3>${title}</h3>
<p>${message}</p>
</div>
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
}, 10);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
});

49
app/templates/404.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面未找到 - 图书管理系统</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<style>
.error-container {
text-align: center;
padding: 50px 20px;
}
.error-code {
font-size: 100px;
font-weight: bold;
color: #4a89dc;
margin-bottom: 20px;
}
.error-message {
font-size: 24px;
color: #333;
margin-bottom: 30px;
}
.back-button {
display: inline-block;
padding: 10px 20px;
background-color: #4a89dc;
color: white;
text-decoration: none;
border-radius: 5px;
font-weight: 500;
}
.back-button:hover {
background-color: #3b78c4;
}
</style>
</head>
<body>
<div class="main-container">
<div class="error-container">
<div class="error-code">404</div>
<div class="error-message">页面未找到</div>
<p>抱歉,您访问的页面不存在或已被移除。</p>
<p style="margin-bottom: 30px;">请检查URL是否正确或返回首页。</p>
<a href="{{ url_for('index') }}" class="back-button">返回首页</a>
</div>
</div>
</body>
</html>

0
app/templates/base.html Normal file
View File

214
app/templates/index.html Normal file
View File

@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页 - 图书管理系统</title>
<!-- 只引用index页面的专用样式 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="app-container">
<!-- 侧边导航栏 -->
<nav class="sidebar">
<div class="logo-container">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" class="logo">
<h2>图书管理系统</h2>
</div>
<ul class="nav-links">
<li class="active"><a href="#"><i class="fas fa-home"></i> 首页</a></li>
<li><a href="#"><i class="fas fa-book"></i> 图书浏览</a></li>
<li><a href="#"><i class="fas fa-bookmark"></i> 我的借阅</a></li>
<li><a href="#"><i class="fas fa-bell"></i> 通知公告</a></li>
{% if current_user.role_id == 1 %}
<li class="nav-category">管理功能</li>
<li><a href="#"><i class="fas fa-users"></i> 用户管理</a></li>
<li><a href="#"><i class="fas fa-layer-group"></i> 图书管理</a></li>
<li><a href="#"><i class="fas fa-exchange-alt"></i> 借阅管理</a></li>
<li><a href="#"><i class="fas fa-warehouse"></i> 库存管理</a></li>
<li><a href="#"><i class="fas fa-chart-bar"></i> 统计分析</a></li>
<li><a href="#"><i class="fas fa-history"></i> 日志管理</a></li>
{% endif %}
</ul>
</nav>
<!-- 主内容区 -->
<main class="main-content">
<!-- 顶部导航 -->
<header class="top-bar">
<div class="search-container">
<i class="fas fa-search search-icon"></i>
<input type="text" placeholder="搜索图书..." class="search-input">
</div>
<div class="user-menu">
<div class="notifications">
<i class="fas fa-bell"></i>
<span class="badge">3</span>
</div>
<div class="user-info">
<div class="user-avatar">
{{ current_user.username[0] }}
</div>
<div class="user-details">
<span class="user-name">{{ current_user.username }}</span>
<span class="user-role">{{ '管理员' if current_user.role_id == 1 else '普通用户' }}</span>
</div>
<div class="dropdown-menu">
<a href="#"><i class="fas fa-user-circle"></i> 个人中心</a>
<a href="#"><i class="fas fa-cog"></i> 设置</a>
<a href="{{ url_for('user.logout') }}"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
</div>
</div>
</div>
</header>
<!-- 欢迎区域 -->
<div class="welcome-section">
<h1>欢迎回来,{{ current_user.username }}</h1>
<p>今天是 <span id="current-date"></span>,祝您使用愉快。</p>
</div>
<!-- 快速统计 -->
<div class="stats-container">
<div class="stat-card">
<i class="fas fa-book stat-icon"></i>
<div class="stat-info">
<h3>馆藏总量</h3>
<p class="stat-number">8,567</p>
</div>
</div>
<div class="stat-card">
<i class="fas fa-users stat-icon"></i>
<div class="stat-info">
<h3>注册用户</h3>
<p class="stat-number">1,245</p>
</div>
</div>
<div class="stat-card">
<i class="fas fa-exchange-alt stat-icon"></i>
<div class="stat-info">
<h3>当前借阅</h3>
<p class="stat-number">352</p>
</div>
</div>
<div class="stat-card">
<i class="fas fa-clock stat-icon"></i>
<div class="stat-info">
<h3>待还图书</h3>
<p class="stat-number">{{ 5 }}</p>
</div>
</div>
</div>
<!-- 主要内容区 -->
<div class="main-sections">
<!-- 最新图书 -->
<div class="content-section book-section">
<div class="section-header">
<h2>最新图书</h2>
<a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
</div>
<div class="book-grid">
{% for i in range(4) %}
<div class="book-card">
<div class="book-cover">
<img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="Book Cover" onerror="this.src='https://via.placeholder.com/150x210?text=No+Cover'">
</div>
<div class="book-info">
<h3 class="book-title">示例图书标题</h3>
<p class="book-author">作者名</p>
<div class="book-meta">
<span class="book-category">计算机</span>
<span class="book-status available">可借阅</span>
</div>
<button class="borrow-btn">借阅</button>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- 通知公告 -->
<div class="content-section notice-section">
<div class="section-header">
<h2>通知公告</h2>
<a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
</div>
<div class="notice-list">
<div class="notice-item">
<div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
<div class="notice-content">
<h3>关于五一假期图书馆开放时间调整的通知</h3>
<p>五一期间(5月1日-5日)图书馆开放时间调整为上午9:00-下午5:00。</p>
<div class="notice-meta">
<span class="notice-time">2023-04-28</span>
</div>
</div>
</div>
<div class="notice-item">
<div class="notice-icon"><i class="fas fa-bell"></i></div>
<div class="notice-content">
<h3>您有2本图书即将到期</h3>
<p>《Python编程》《算法导论》将于3天后到期请及时归还或办理续借。</p>
<div class="notice-meta">
<span class="notice-time">2023-04-27</span>
<button class="renew-btn">一键续借</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 热门图书区域 -->
<div class="content-section popular-section">
<div class="section-header">
<h2>热门图书</h2>
<a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
</div>
<div class="popular-books">
{% for i in range(5) %}
<div class="popular-book-item">
<div class="rank-badge">{{ i+1 }}</div>
<div class="book-cover small">
<img src="https://via.placeholder.com/80x120?text=Book" alt="Book Cover">
</div>
<div class="book-details">
<h3 class="book-title">热门图书标题示例</h3>
<p class="book-author">知名作者</p>
<div class="book-stats">
<span><i class="fas fa-eye"></i> 1024 次浏览</span>
<span><i class="fas fa-bookmark"></i> 89 次借阅</span>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</main>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 设置当前日期
const now = new Date();
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
document.getElementById('current-date').textContent = now.toLocaleDateString('zh-CN', options);
// 用户菜单下拉
const userInfo = document.querySelector('.user-info');
userInfo.addEventListener('click', function(e) {
userInfo.classList.toggle('active');
});
// 点击其他区域关闭下拉菜单
document.addEventListener('click', function(e) {
if (!userInfo.contains(e.target)) {
userInfo.classList.remove('active');
}
});
});
</script>
</body>
</html>

92
app/templates/login.html Normal file
View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录 - 图书管理系统</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
</head>
<body>
<div class="overlay"></div>
<div class="theme-toggle" id="theme-toggle">☀️</div>
<div class="main-container">
<div class="login-container">
<div class="logo">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
</div>
<h1>图书管理系统</h1>
<p class="subtitle">欢迎回来,请登录您的账户</p>
<div id="account-login">
<form id="login-form" action="{{ url_for('user.login') }}" method="post">
<div class="form-group">
<label for="username">用户名/邮箱</label>
<div class="input-with-icon">
<span class="input-icon">👤</span>
<input type="text" id="username" name="username" class="form-control" placeholder="请输入账号">
</div>
<div class="validation-message" id="username-error"></div>
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="input-with-icon">
<span class="input-icon">🔒</span>
<input type="password" id="password" name="password" class="form-control" placeholder="请输入密码">
<span class="password-toggle" id="password-toggle">👁️</span>
</div>
<div class="validation-message" id="password-error"></div>
</div>
<div class="remember-forgot">
<label class="custom-checkbox">
<input type="checkbox" name="remember_me">
<span class="checkmark"></span>
记住我 (7天内免登录)
</label>
<div class="forgot-password">
<a href="#">忘记密码?</a>
</div>
</div>
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<button type="submit" class="btn-login" id="login-button">
<span>登录</span>
<span class="loading"></span>
</button>
</form>
<div class="signup">
还没有账号? <a href="{{ url_for('user.register') }}">立即注册</a>
</div>
<div class="features">
<div class="feature-item">
<span class="feature-icon">🔒</span>
<span>安全登录</span>
</div>
<div class="feature-item">
<span class="feature-icon">🔐</span>
<span>数据加密</span>
</div>
<div class="feature-item">
<span class="feature-icon">📚</span>
<span>图书管理</span>
</div>
</div>
</div>
</div>
</div>
<footer>
<p>© 2025 图书管理系统 - 版权所有</p>
</footer>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>

View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户注册 - 图书管理系统</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
</head>
<body>
<div class="overlay"></div>
<div class="theme-toggle" id="theme-toggle">☀️</div>
<div class="main-container">
<div class="login-container register-container">
<div class="logo">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
</div>
<h1>图书管理系统</h1>
<p class="subtitle">创建您的新账户</p>
<div id="register-form-container">
<form id="register-form" action="{{ url_for('user.register') }}" method="post">
<div class="form-group">
<label for="username">用户名</label>
<div class="input-with-icon">
<span class="input-icon">👤</span>
<input type="text" id="username" name="username" class="form-control" placeholder="请输入用户名" required>
</div>
<div class="validation-message" id="username-error"></div>
</div>
<div class="form-group">
<label for="email">邮箱</label>
<div class="input-with-icon">
<span class="input-icon">📧</span>
<input type="email" id="email" name="email" class="form-control" placeholder="请输入邮箱" required>
</div>
<div class="validation-message" id="email-error"></div>
</div>
<div class="form-group">
<label for="verification_code">邮箱验证码</label>
<div class="verification-code-container">
<input type="text" id="verification_code" name="verification_code" class="verification-input" placeholder="请输入验证码" required>
<button type="button" id="send-code-btn" class="send-code-btn">发送验证码</button>
</div>
<div class="validation-message" id="verification-code-error"></div>
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="input-with-icon">
<span class="input-icon">🔒</span>
<input type="password" id="password" name="password" class="form-control" placeholder="请设置密码" required>
<span class="password-toggle" id="password-toggle">👁️</span>
</div>
<div class="validation-message" id="password-error"></div>
</div>
<div class="form-group">
<label for="confirm_password">确认密码</label>
<div class="input-with-icon">
<span class="input-icon">🔒</span>
<input type="password" id="confirm_password" name="confirm_password" class="form-control" placeholder="请再次输入密码" required>
</div>
<div class="validation-message" id="confirm-password-error"></div>
</div>
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<button type="submit" class="btn-login" id="register-button">
<span>注册</span>
<span class="loading"></span>
</button>
</form>
<div class="signup">
已有账号? <a href="{{ url_for('user.login') }}">返回登录</a>
</div>
</div>
</div>
</div>
<footer>
<p>© 2025 图书管理系统 - 版权所有</p>
</footer>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>

0
app/utils/__init__.py Normal file
View File

0
app/utils/auth.py Normal file
View File

0
app/utils/db.py Normal file
View File

91
app/utils/email.py Normal file
View File

@ -0,0 +1,91 @@
import smtplib
import random
import string
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from flask import current_app
import logging
# 配置日志
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# 配置邮件发送功能
def send_verification_email(to_email, verification_code):
"""
发送验证码邮件
"""
try:
# 从应用配置获取邮件设置
email_host = current_app.config['EMAIL_HOST']
email_port = current_app.config['EMAIL_PORT']
email_username = current_app.config['EMAIL_USERNAME']
email_password = current_app.config['EMAIL_PASSWORD']
email_from = current_app.config['EMAIL_FROM']
email_from_name = current_app.config['EMAIL_FROM_NAME']
logger.info(f"准备发送邮件到: {to_email}, 验证码: {verification_code}")
logger.debug(f"邮件配置: 主机={email_host}, 端口={email_port}")
# 邮件内容
msg = MIMEMultipart()
msg['From'] = f"{email_from_name} <{email_from}>"
msg['To'] = to_email
msg['Subject'] = "图书管理系统 - 验证码"
# 邮件正文
body = f"""
<html>
<body>
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e1e1e1; border-radius: 5px;">
<h2 style="color: #4a89dc;">图书管理系统 - 邮箱验证</h2>
<p>您好</p>
<p>感谢您注册图书管理系统您的验证码是:</p>
<div style="background-color: #f5f5f5; padding: 10px; border-radius: 5px; text-align: center; font-size: 24px; letter-spacing: 5px; font-weight: bold; margin: 20px 0;">
{verification_code}
</div>
<p>该验证码将在10分钟内有效请勿将验证码分享给他人</p>
<p>如果您没有请求此验证码请忽略此邮件</p>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e1e1e1; font-size: 12px; color: #888;">
<p>此邮件为系统自动发送请勿回复</p>
<p>&copy; 2025 图书管理系统</p>
</div>
</div>
</body>
</html>
"""
msg.attach(MIMEText(body, 'html'))
logger.debug("尝试连接到SMTP服务器...")
# 连接服务器发送邮件
server = smtplib.SMTP(email_host, email_port)
server.set_debuglevel(1) # 启用详细的SMTP调试输出
logger.debug("检查是否需要STARTTLS加密...")
if current_app.config.get('EMAIL_ENCRYPTION') == 'starttls':
logger.debug("启用STARTTLS...")
server.starttls()
logger.debug(f"尝试登录邮箱: {email_username}")
server.login(email_username, email_password)
logger.debug("发送邮件...")
server.send_message(msg)
logger.debug("关闭连接...")
server.quit()
logger.info(f"邮件发送成功: {to_email}")
return True
except Exception as e:
logger.error(f"邮件发送失败: {str(e)}", exc_info=True)
return False
def generate_verification_code(length=6):
"""
生成数字验证码
"""
return ''.join(random.choice(string.digits) for _ in range(length))

0
app/utils/helpers.py Normal file
View File

2640
code_collection.txt Normal file

File diff suppressed because it is too large Load Diff

27
config.py Normal file
View File

@ -0,0 +1,27 @@
import os
# 数据库配置
DB_HOST = os.environ.get('DB_HOST', '27.124.22.104')
DB_PORT = os.environ.get('DB_PORT', '3306')
DB_USER = os.environ.get('DB_USER', 'book20250428')
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'booksystem')
DB_NAME = os.environ.get('DB_NAME', 'book_system')
# 数据库连接字符串
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 应用密钥
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev_key_replace_in_production')
# 邮件配置
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.qq.com')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
EMAIL_ENCRYPTION = os.environ.get('EMAIL_ENCRYPTION', 'starttls')
EMAIL_USERNAME = os.environ.get('EMAIL_USERNAME', '3399560459@qq.com')
EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD', 'fzwhyirhbqdzcjgf')
EMAIL_FROM = os.environ.get('EMAIL_FROM', '3399560459@qq.com')
EMAIL_FROM_NAME = os.environ.get('EMAIL_FROM_NAME', 'BOOKSYSTEM_OFFICIAL')
# 会话配置
PERMANENT_SESSION_LIFETIME = 86400 * 7

16
main.py Normal file
View File

@ -0,0 +1,16 @@
# 这是一个示例 Python 脚本。
# 按 ⌃R 执行或将其替换为您的代码。
# 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。
def print_hi(name):
# 在下面的代码行中使用断点来调试脚本。
print(f'Hi, {name}') # 按 ⌘F8 切换断点。
# 按间距中的绿色按钮以运行脚本。
if __name__ == '__main__':
print_hi('PyCharm')
# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
Flask==2.3.3
Flask-SQLAlchemy==3.1.1
pymysql==1.1.0
Werkzeug==2.3.7
email-validator==2.1.0.post1
cryptography

193
sql/book_system.sql Normal file
View File

@ -0,0 +1,193 @@
/*
Navicat Premium Dump SQL
Source Server : Book_system
Source Server Type : MySQL
Source Server Version : 80400 (8.4.0)
Source Host : 27.124.22.104:3306
Source Schema : book_system
Target Server Type : MySQL
Target Server Version : 80400 (8.4.0)
File Encoding : 65001
Date: 29/04/2025 00:41:53
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for announcements
-- ----------------------------
DROP TABLE IF EXISTS `announcements`;
CREATE TABLE `announcements` (
`id` int NOT NULL AUTO_INCREMENT,
`title` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
`content` text COLLATE utf8mb4_general_ci NOT NULL,
`publisher_id` int NOT NULL,
`is_top` tinyint DEFAULT '0',
`status` tinyint DEFAULT '1',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `publisher_id` (`publisher_id`),
CONSTRAINT `announcements_ibfk_1` FOREIGN KEY (`publisher_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Table structure for books
-- ----------------------------
DROP TABLE IF EXISTS `books`;
CREATE TABLE `books` (
`id` int NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`author` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
`publisher` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
`category_id` int DEFAULT NULL,
`tags` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`isbn` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
`publish_year` varchar(16) COLLATE utf8mb4_general_ci DEFAULT NULL,
`description` text COLLATE utf8mb4_general_ci,
`cover_url` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`stock` int DEFAULT '0',
`price` decimal(10,2) DEFAULT NULL,
`status` tinyint DEFAULT '1',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `isbn` (`isbn`),
KEY `category_id` (`category_id`),
CONSTRAINT `books_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Table structure for borrow_records
-- ----------------------------
DROP TABLE IF EXISTS `borrow_records`;
CREATE TABLE `borrow_records` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`book_id` int NOT NULL,
`borrow_date` datetime NOT NULL,
`due_date` datetime NOT NULL,
`return_date` datetime DEFAULT NULL,
`renew_count` int DEFAULT '0',
`status` tinyint DEFAULT '1',
`remark` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `book_id` (`book_id`),
CONSTRAINT `borrow_records_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `borrow_records_ibfk_2` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Table structure for categories
-- ----------------------------
DROP TABLE IF EXISTS `categories`;
CREATE TABLE `categories` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
`parent_id` int DEFAULT NULL,
`sort` int DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Table structure for inventory_logs
-- ----------------------------
DROP TABLE IF EXISTS `inventory_logs`;
CREATE TABLE `inventory_logs` (
`id` int NOT NULL AUTO_INCREMENT,
`book_id` int NOT NULL,
`change_type` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
`change_amount` int NOT NULL,
`after_stock` int NOT NULL,
`operator_id` int DEFAULT NULL,
`remark` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`changed_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `book_id` (`book_id`),
KEY `operator_id` (`operator_id`),
CONSTRAINT `inventory_logs_ibfk_1` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`),
CONSTRAINT `inventory_logs_ibfk_2` FOREIGN KEY (`operator_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Table structure for logs
-- ----------------------------
DROP TABLE IF EXISTS `logs`;
CREATE TABLE `logs` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int DEFAULT NULL,
`action` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
`target_type` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
`target_id` int DEFAULT NULL,
`ip_address` varchar(45) COLLATE utf8mb4_general_ci DEFAULT NULL,
`description` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `logs_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Table structure for notifications
-- ----------------------------
DROP TABLE IF EXISTS `notifications`;
CREATE TABLE `notifications` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`title` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
`content` text COLLATE utf8mb4_general_ci NOT NULL,
`type` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
`status` tinyint DEFAULT '0',
`sender_id` int DEFAULT NULL,
`created_at` datetime NOT NULL,
`read_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `sender_id` (`sender_id`),
CONSTRAINT `notifications_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `notifications_ibfk_2` FOREIGN KEY (`sender_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Table structure for roles
-- ----------------------------
DROP TABLE IF EXISTS `roles`;
CREATE TABLE `roles` (
`id` int NOT NULL AUTO_INCREMENT,
`role_name` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
`description` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `role_name` (`role_name`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
`password` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`email` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
`phone` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
`nickname` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
`status` tinyint DEFAULT '1',
`role_id` int NOT NULL DEFAULT '2',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`),
UNIQUE KEY `phone` (`phone`),
KEY `role_id` (`role_id`),
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
SET FOREIGN_KEY_CHECKS = 1;

28
sql/module1_user.sql Normal file
View File

@ -0,0 +1,28 @@
-- 角色表
CREATE TABLE `roles` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`role_name` VARCHAR(32) NOT NULL UNIQUE,
`description` VARCHAR(128)
);
-- 用户表
CREATE TABLE `users` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(64) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL,
`email` VARCHAR(128) UNIQUE,
`phone` VARCHAR(20) UNIQUE,
`nickname` VARCHAR(64),
`status` TINYINT DEFAULT 1,
`role_id` INT NOT NULL DEFAULT 2,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`)
);
-- (可选)初始化角色数据
INSERT INTO `roles` (`role_name`, `description`) VALUES
('admin', '管理员'),
('user', '普通用户');

47
sql/module2_book_info.sql Normal file
View File

@ -0,0 +1,47 @@
-- 分类表
CREATE TABLE `categories` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(64) NOT NULL,
`parent_id` INT DEFAULT NULL, -- 支持多级分类。顶级分类parent_id为NULL
`sort` INT DEFAULT 0 -- 排序字段,可选
);
-- 图书信息表
CREATE TABLE `books` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`title` VARCHAR(255) NOT NULL, -- 书名
`author` VARCHAR(128) NOT NULL, -- 作者
`publisher` VARCHAR(128), -- 出版社
`category_id` INT, -- 分类外键
`tags` VARCHAR(255), -- 标签(字符串,逗号分隔,可选)
`isbn` VARCHAR(32) UNIQUE, -- ISBN
`publish_year` VARCHAR(16), -- 出版年份
`description` TEXT, -- 简介
`cover_url` VARCHAR(255), -- 封面图片地址
`stock` INT DEFAULT 0, -- 库存
`price` DECIMAL(10,2), -- 定价
`status` TINYINT DEFAULT 1, -- 1=正常0=删除
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`)
);
INSERT INTO `categories` (`name`, `parent_id`, `sort`) VALUES
('文学', NULL, 1),
('小说', 1, 1),
('散文', 1, 2),
('计算机', NULL, 2),
('编程', 4, 1),
('人工智能', 4, 2),
('历史', NULL, 3),
('艺术', NULL, 4);
INSERT INTO `books`
(`title`, `author`, `publisher`, `category_id`, `tags`, `isbn`, `publish_year`, `description`, `cover_url`, `stock`, `price`, `status`, `created_at`, `updated_at`)
VALUES
('三体', '刘慈欣', '重庆出版社', 2, '科幻,宇宙', '9787229100605', '2008', '中国著名科幻小说,三体世界的故事。', '/covers/santi.jpg', 10, 45.00, 1, NOW(), NOW()),
('解忧杂货店', '东野圭吾', '南海出版公司', 1, '治愈,悬疑', '9787544270878', '2014', '通过信件为人们解忧的杂货店故事。', '/covers/jieyou.jpg', 5, 39.80, 1, NOW(), NOW()),
('Python编程从入门到实践', 'Eric Matthes', '人民邮电出版社', 5, '编程,Python', '9787115428028', '2016', '一本面向编程初学者的Python实践书籍。', '/covers/python_book.jpg', 8, 59.00, 1, NOW(), NOW()),
('人工智能简史', '尼克·博斯特罗姆', '浙江人民出版社', 6, 'AI,未来', '9787213064325', '2018', '人工智能发展的历史及其未来展望。', '/covers/ai_history.jpg', 6, 68.00, 1, NOW(), NOW()),
('百年孤独', '加西亚·马尔克斯', '南海出版公司', 2, '魔幻现实主义', '9787544291170', '2011', '魔幻现实主义经典小说。', '/covers/bainiangudu.jpg', 3, 58.00, 1, NOW(), NOW()),
('中国通史', '吕思勉', '中华书局', 7, '历史,中国史', '9787101125455', '2017', '中国历史发展脉络全面梳理。', '/covers/zhongguotongshi.jpg', 7, 49.80, 1, NOW(), NOW());

View File

@ -0,0 +1,17 @@
-- 借阅表
CREATE TABLE `borrow_records` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL, -- 借阅人用户id
`book_id` INT NOT NULL, -- 图书id
`borrow_date` DATETIME NOT NULL, -- 借书时间
`due_date` DATETIME NOT NULL, -- 应还日期
`return_date` DATETIME DEFAULT NULL, -- 实际归还(未归还为空)
`renew_count` INT DEFAULT 0, -- 续借次数
`status` TINYINT DEFAULT 1, -- 1:借出 2:已归还 3:逾期未还
`remark` VARCHAR(255), -- 管理备注
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`book_id`) REFERENCES `books`(`id`)
);

View File

@ -0,0 +1,13 @@
-- 库存变动明细表
CREATE TABLE `inventory_logs` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`book_id` INT NOT NULL, -- 图书id
`change_type` VARCHAR(32) NOT NULL, -- 变动类型
`change_amount` INT NOT NULL, -- 变动数量
`after_stock` INT NOT NULL, -- 变动后的库存
`operator_id` INT, -- 操作人id
`remark` VARCHAR(255), -- 备注
`changed_at` DATETIME NOT NULL, -- 变动时间
FOREIGN KEY (`book_id`) REFERENCES `books`(`id`),
FOREIGN KEY (`operator_id`) REFERENCES `users`(`id`)
);

View File

@ -0,0 +1,28 @@
-- 系统公告表
CREATE TABLE `announcements` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`title` VARCHAR(128) NOT NULL,
`content` TEXT NOT NULL,
`publisher_id` INT NOT NULL,
`is_top` TINYINT DEFAULT 0, -- 是否置顶
`status` TINYINT DEFAULT 1, -- 1有效 0撤回/禁用
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
FOREIGN KEY (`publisher_id`) REFERENCES `users`(`id`)
);
-- 用户消息通知表
CREATE TABLE `notifications` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`title` VARCHAR(128) NOT NULL,
`content` TEXT NOT NULL,
`type` VARCHAR(32) NOT NULL, -- 消息类型
`status` TINYINT DEFAULT 0, -- 0未读 1已读
`sender_id` INT, -- 发送人系统消息可为NULL或0
`created_at` DATETIME NOT NULL,
`read_at` DATETIME DEFAULT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`sender_id`) REFERENCES `users`(`id`)
);

View File

@ -0,0 +1,12 @@
-- 日志管理表
CREATE TABLE `logs` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT, -- 操作者id
`action` VARCHAR(64) NOT NULL, -- 操作名
`target_type` VARCHAR(32), -- 对象类型
`target_id` INT, -- 对象id
`ip_address` VARCHAR(45), -- 操作来源ip
`description` VARCHAR(255), -- 补充描述
`created_at` DATETIME NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
);