diff --git a/all_file_output.py b/all_file_output.py new file mode 100644 index 0000000..ef4d03d --- /dev/null +++ b/all_file_output.py @@ -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) \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..e95008d --- /dev/null +++ b/app.py @@ -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) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..41e38d3 --- /dev/null +++ b/app/__init__.py @@ -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 \ No newline at end of file diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/announcement.py b/app/controllers/announcement.py new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/book.py b/app/controllers/book.py new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/borrow.py b/app/controllers/borrow.py new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/inventory.py b/app/controllers/inventory.py new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/log.py b/app/controllers/log.py new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/statistics.py b/app/controllers/statistics.py new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/user.py b/app/controllers/user.py new file mode 100644 index 0000000..a0561f4 --- /dev/null +++ b/app/controllers/user.py @@ -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': '邮件发送失败,请稍后重试'}) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/announcement.py b/app/models/announcement.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/book.py b/app/models/book.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/borrow.py b/app/models/borrow.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/inventory.py b/app/models/inventory.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/log.py b/app/models/log.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/notification.py b/app/models/notification.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..d0182cc --- /dev/null +++ b/app/models/user.py @@ -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') diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/book_service.py b/app/services/book_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/borrow_service.py b/app/services/borrow_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/inventory_service.py b/app/services/inventory_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/user_service.py b/app/services/user_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/static/css/index.css b/app/static/css/index.css new file mode 100644 index 0000000..f0896b9 --- /dev/null +++ b/app/static/css/index.css @@ -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; + } +} \ No newline at end of file diff --git a/app/static/css/main.css b/app/static/css/main.css new file mode 100644 index 0000000..79fdbfa --- /dev/null +++ b/app/static/css/main.css @@ -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; +} diff --git a/app/static/images/logo.png b/app/static/images/logo.png new file mode 100644 index 0000000..d1e40dc Binary files /dev/null and b/app/static/images/logo.png differ diff --git a/app/static/js/main.js b/app/static/js/main.js new file mode 100644 index 0000000..95937b2 --- /dev/null +++ b/app/static/js/main.js @@ -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 = ` +
${icon}
+
+

${title}

+

${message}

+
+ `; + + document.body.appendChild(notification); + + setTimeout(() => { + notification.classList.add('show'); + }, 10); + + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => { + document.body.removeChild(notification); + }, 300); + }, 3000); + } +}); \ No newline at end of file diff --git a/app/templates/404.html b/app/templates/404.html new file mode 100644 index 0000000..1a08b9c --- /dev/null +++ b/app/templates/404.html @@ -0,0 +1,49 @@ + + + + + + 页面未找到 - 图书管理系统 + + + + +
+
+
404
+
页面未找到
+

抱歉,您访问的页面不存在或已被移除。

+

请检查URL是否正确,或返回首页。

+ 返回首页 +
+
+ + diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..4745a9a --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,214 @@ + + + + + + 首页 - 图书管理系统 + + + + + +
+ + + + +
+ +
+
+ + +
+
+
+ + 3 +
+ +
+
+ + +
+

欢迎回来,{{ current_user.username }}!

+

今天是 ,祝您使用愉快。

+
+ + +
+
+ +
+

馆藏总量

+

8,567

+
+
+
+ +
+

注册用户

+

1,245

+
+
+
+ +
+

当前借阅

+

352

+
+
+
+ +
+

待还图书

+

{{ 5 }}

+
+
+
+ + +
+ +
+
+

最新图书

+ 查看全部 +
+
+ {% for i in range(4) %} +
+
+ Book Cover +
+
+

示例图书标题

+

作者名

+
+ 计算机 + 可借阅 +
+ +
+
+ {% endfor %} +
+
+ + +
+
+

通知公告

+ 查看全部 +
+
+
+
+
+

关于五一假期图书馆开放时间调整的通知

+

五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。

+
+ 2023-04-28 +
+
+
+
+
+
+

您有2本图书即将到期

+

《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。

+
+ 2023-04-27 + +
+
+
+
+
+
+ + + +
+
+ + + + \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..223d27c --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,92 @@ + + + + + + 用户登录 - 图书管理系统 + + + +
+ +
☀️
+ +
+
+ +

图书管理系统

+

欢迎回来,请登录您的账户

+ +
+
+
+ +
+ 👤 + +
+
+
+ +
+ +
+ 🔒 + + 👁️ +
+
+
+ +
+ + +
+ + {% if error %} +
{{ error }}
+ {% endif %} + + +
+ + + +
+
+ 🔒 + 安全登录 +
+
+ 🔐 + 数据加密 +
+
+ 📚 + 图书管理 +
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/app/templates/register.html b/app/templates/register.html new file mode 100644 index 0000000..f1b3f90 --- /dev/null +++ b/app/templates/register.html @@ -0,0 +1,93 @@ + + + + + + 用户注册 - 图书管理系统 + + + +
+ +
☀️
+ +
+
+ +

图书管理系统

+

创建您的新账户

+ +
+
+
+ +
+ 👤 + +
+
+
+ +
+ +
+ 📧 + +
+
+
+ +
+ +
+ + +
+
+
+ +
+ +
+ 🔒 + + 👁️ +
+
+
+ +
+ +
+ 🔒 + +
+
+
+ + {% if error %} +
{{ error }}
+ {% endif %} + + +
+ + +
+
+
+ + + + + + \ No newline at end of file diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/auth.py b/app/utils/auth.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/db.py b/app/utils/db.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/email.py b/app/utils/email.py new file mode 100644 index 0000000..36f95ce --- /dev/null +++ b/app/utils/email.py @@ -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""" + + +
+

图书管理系统 - 邮箱验证

+

您好,

+

感谢您注册图书管理系统,您的验证码是:

+
+ {verification_code} +
+

该验证码将在10分钟内有效,请勿将验证码分享给他人。

+

如果您没有请求此验证码,请忽略此邮件。

+
+

此邮件为系统自动发送,请勿回复。

+

© 2025 图书管理系统

+
+
+ + + """ + + 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)) diff --git a/app/utils/helpers.py b/app/utils/helpers.py new file mode 100644 index 0000000..e69de29 diff --git a/code_collection.txt b/code_collection.txt new file mode 100644 index 0000000..22f4572 --- /dev/null +++ b/code_collection.txt @@ -0,0 +1,2640 @@ + +================================================================================ +File: ./config.py +================================================================================ + +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 +================================================================================ +File: ./all_file_output.py +================================================================================ + +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) +================================================================================ +File: ./app.py +================================================================================ + +from app import create_app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=49666) + +================================================================================ +File: ./main.py +================================================================================ + +# 这是一个示例 Python 脚本。 + +# 按 ⌃R 执行或将其替换为您的代码。 +# 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。 + + +def print_hi(name): + # 在下面的代码行中使用断点来调试脚本。 + print(f'Hi, {name}') # 按 ⌘F8 切换断点。 + + +# 按间距中的绿色按钮以运行脚本。 +if __name__ == '__main__': + print_hi('PyCharm') + +# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助 + +================================================================================ +File: ./app/__init__.py +================================================================================ + +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 +================================================================================ +File: ./app/utils/auth.py +================================================================================ + + +================================================================================ +File: ./app/utils/db.py +================================================================================ + + +================================================================================ +File: ./app/utils/__init__.py +================================================================================ + + +================================================================================ +File: ./app/utils/email.py +================================================================================ + +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""" + + +
+

图书管理系统 - 邮箱验证

+

您好,

+

感谢您注册图书管理系统,您的验证码是:

+
+ {verification_code} +
+

该验证码将在10分钟内有效,请勿将验证码分享给他人。

+

如果您没有请求此验证码,请忽略此邮件。

+
+

此邮件为系统自动发送,请勿回复。

+

© 2025 图书管理系统

+
+
+ + + """ + + 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)) + +================================================================================ +File: ./app/utils/helpers.py +================================================================================ + + +================================================================================ +File: ./app/models/user.py +================================================================================ + +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') + +================================================================================ +File: ./app/models/log.py +================================================================================ + + +================================================================================ +File: ./app/models/notification.py +================================================================================ + + +================================================================================ +File: ./app/models/__init__.py +================================================================================ + + +================================================================================ +File: ./app/models/book.py +================================================================================ + + +================================================================================ +File: ./app/models/borrow.py +================================================================================ + + +================================================================================ +File: ./app/models/announcement.py +================================================================================ + + +================================================================================ +File: ./app/models/inventory.py +================================================================================ + + +================================================================================ +File: ./app/static/css/index.css +================================================================================ + +/* 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; + } +} +================================================================================ +File: ./app/static/css/main.css +================================================================================ + +/* 主样式文件 - 从登录页面复制过来的样式 */ +/* 从您提供的登录页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; +} + +================================================================================ +File: ./app/static/js/main.js +================================================================================ + +// 主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 = ` +
${icon}
+
+

${title}

+

${message}

+
+ `; + + document.body.appendChild(notification); + + setTimeout(() => { + notification.classList.add('show'); + }, 10); + + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => { + document.body.removeChild(notification); + }, 300); + }, 3000); + } +}); +================================================================================ +File: ./app/templates/index.html +================================================================================ + + + + + + + 首页 - 图书管理系统 + + + + + +
+ + + + +
+ +
+
+ + +
+
+
+ + 3 +
+ +
+
+ + +
+

欢迎回来,{{ current_user.username }}!

+

今天是 ,祝您使用愉快。

+
+ + +
+
+ +
+

馆藏总量

+

8,567

+
+
+
+ +
+

注册用户

+

1,245

+
+
+
+ +
+

当前借阅

+

352

+
+
+
+ +
+

待还图书

+

{{ 5 }}

+
+
+
+ + +
+ +
+
+

最新图书

+ 查看全部 +
+
+ {% for i in range(4) %} +
+
+ Book Cover +
+
+

示例图书标题

+

作者名

+
+ 计算机 + 可借阅 +
+ +
+
+ {% endfor %} +
+
+ + +
+
+

通知公告

+ 查看全部 +
+
+
+
+
+

关于五一假期图书馆开放时间调整的通知

+

五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。

+
+ 2023-04-28 +
+
+
+
+
+
+

您有2本图书即将到期

+

《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。

+
+ 2023-04-27 + +
+
+
+
+
+
+ + + +
+
+ + + + +================================================================================ +File: ./app/templates/base.html +================================================================================ + + +================================================================================ +File: ./app/templates/register.html +================================================================================ + + + + + + + 用户注册 - 图书管理系统 + + + +
+ +
☀️
+ +
+
+ +

图书管理系统

+

创建您的新账户

+ +
+
+
+ +
+ 👤 + +
+
+
+ +
+ +
+ 📧 + +
+
+
+ +
+ +
+ + +
+
+
+ +
+ +
+ 🔒 + + 👁️ +
+
+
+ +
+ +
+ 🔒 + +
+
+
+ + {% if error %} +
{{ error }}
+ {% endif %} + + +
+ + +
+
+
+ + + + + + +================================================================================ +File: ./app/templates/404.html +================================================================================ + + + + + + + 页面未找到 - 图书管理系统 + + + + +
+
+
404
+
页面未找到
+

抱歉,您访问的页面不存在或已被移除。

+

请检查URL是否正确,或返回首页。

+ 返回首页 +
+
+ + + +================================================================================ +File: ./app/templates/login.html +================================================================================ + + + + + + + 用户登录 - 图书管理系统 + + + +
+ +
☀️
+ +
+
+ +

图书管理系统

+

欢迎回来,请登录您的账户

+ +
+
+
+ +
+ 👤 + +
+
+
+ +
+ +
+ 🔒 + + 👁️ +
+
+
+ +
+ + +
+ + {% if error %} +
{{ error }}
+ {% endif %} + + +
+ + + +
+
+ 🔒 + 安全登录 +
+
+ 🔐 + 数据加密 +
+
+ 📚 + 图书管理 +
+
+
+
+
+ + + + + + +================================================================================ +File: ./app/controllers/user.py +================================================================================ + +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': '邮件发送失败,请稍后重试'}) + +================================================================================ +File: ./app/controllers/log.py +================================================================================ + + +================================================================================ +File: ./app/controllers/__init__.py +================================================================================ + + +================================================================================ +File: ./app/controllers/book.py +================================================================================ + + +================================================================================ +File: ./app/controllers/statistics.py +================================================================================ + + +================================================================================ +File: ./app/controllers/borrow.py +================================================================================ + + +================================================================================ +File: ./app/controllers/announcement.py +================================================================================ + + +================================================================================ +File: ./app/controllers/inventory.py +================================================================================ + + +================================================================================ +File: ./app/services/borrow_service.py +================================================================================ + + +================================================================================ +File: ./app/services/inventory_service.py +================================================================================ + + +================================================================================ +File: ./app/services/__init__.py +================================================================================ + + +================================================================================ +File: ./app/services/book_service.py +================================================================================ + + +================================================================================ +File: ./app/services/user_service.py +================================================================================ + diff --git a/config.py b/config.py new file mode 100644 index 0000000..9e301c2 --- /dev/null +++ b/config.py @@ -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 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..5b25c56 --- /dev/null +++ b/main.py @@ -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 帮助 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..89e0e90 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/sql/book_system.sql b/sql/book_system.sql new file mode 100644 index 0000000..10148e4 --- /dev/null +++ b/sql/book_system.sql @@ -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; diff --git a/sql/module1_user.sql b/sql/module1_user.sql new file mode 100644 index 0000000..3236908 --- /dev/null +++ b/sql/module1_user.sql @@ -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', '普通用户'); + + diff --git a/sql/module2_book_info.sql b/sql/module2_book_info.sql new file mode 100644 index 0000000..786a034 --- /dev/null +++ b/sql/module2_book_info.sql @@ -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()); diff --git a/sql/module3_borrow_record.sql b/sql/module3_borrow_record.sql new file mode 100644 index 0000000..aff55cf --- /dev/null +++ b/sql/module3_borrow_record.sql @@ -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`) +); + diff --git a/sql/module4_inventory_logs.sql b/sql/module4_inventory_logs.sql new file mode 100644 index 0000000..77cc9c7 --- /dev/null +++ b/sql/module4_inventory_logs.sql @@ -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`) +); diff --git a/sql/module5_system_announcement.sql b/sql/module5_system_announcement.sql new file mode 100644 index 0000000..ad320e4 --- /dev/null +++ b/sql/module5_system_announcement.sql @@ -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`) +); + diff --git a/sql/module7_system_log.sql b/sql/module7_system_log.sql new file mode 100644 index 0000000..2953834 --- /dev/null +++ b/sql/module7_system_log.sql @@ -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`) +); \ No newline at end of file