项目一:Python实现PDF增删改查编辑保存功能的全栈解决方案

1、需求

公司文档涉及PDF类型的比较多,需要PDF编辑内容,并且可批量定制化满足业务灵活自主的自动化处理功能。

2、产品设计

提供一个完整的全栈解决方案,包含前端界面和后端服务,实现PDF文档的增删改查(CRUD)和编辑保存功能。

  1. 完整的CRUD功能

    • 创建:上传PDF文件

    • 读取:查看PDF列表和详情

    • 更新:编辑PDF(旋转、水印、提取页面)

    • 删除:删除PDF文件

  2. 用户认证

    • 注册、登录、注销功能

    • 用户只能访问自己的PDF文件

  3. PDF编辑功能

    • 旋转PDF页面

    • 添加水印

    • 提取特定页面

    • 下载处理后的文件

  4. 响应式设计

    • 适配不同屏幕尺寸

    • 现代化的用户界面

  5. 安全特性

    • 文件类型验证

    • 文件安全存储

    • CSRF保护

3. 系统架构

pdf_manager/
├── app/                      # 应用主目录
│   ├── __init__.py          # Flask应用工厂
│   ├── models.py            # 数据库模型
│   ├── routes.py            # 路由和视图函数
│   ├── utils/               # 实用工具
│   │   ├── pdf_utils.py     # PDF处理工具
│   │   └── auth.py         # 认证工具
│   ├── templates/           # HTML模板
│   │   ├── base.html       # 基础模板
│   │   ├── index.html      # 主页
│   │   ├── view.html      # 查看PDF
│   │   ├── edit.html      # 编辑PDF
│   │   └── auth/         # 认证相关模板
│   │       ├── login.html
│   │       └── register.html
│   └── static/             # 静态文件
│       ├── css/
│       │   └── main.css   # 主样式
│       └── js/
│           ├── main.js    # 主JavaScript
│           └── pdf.js    # PDF操作相关JS
├── config.py               # 配置文件
├── requirements.txt        # 依赖文件
└── run.py                  # 启动脚本

2. 后端实现 (Flask)

2.1 配置文件 (config.py)

import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    SECRET_KEY = os.getenv('SECRET_KEY', 'dev-key')
    SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///pdf_manager.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')
    PROCESSED_FOLDER = os.path.join(os.path.dirname(__file__), 'processed')
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 16MB
    ALLOWED_EXTENSIONS = {'pdf'}

2.2 PDF工具类 (app/utils/pdf_utils.py)

import os
from PyPDF2 import PdfFileReader, PdfFileWriter
from reportlab.pdfgen import canvas
import io

class PDFEditor:
    @staticmethod
    def rotate_pdf(input_path, output_path, angle=90):
        """旋转PDF页面"""
        pdf_reader = PdfFileReader(input_path)
        pdf_writer = PdfFileWriter()
        
        for page_num in range(pdf_reader.getNumPages()):
            page = pdf_reader.getPage(page_num)
            page.rotateClockwise(angle)
            pdf_writer.addPage(page)
        
        with open(output_path, 'wb') as out_pdf:
            pdf_writer.write(out_pdf)
        return output_path

    @staticmethod
    def add_watermark(input_path, output_path, watermark_text):
        """添加水印"""
        pdf_reader = PdfFileReader(input_path)
        pdf_writer = PdfFileWriter()
        
        for page_num in range(pdf_reader.getNumPages()):
            page = pdf_reader.getPage(page_num)
            
            packet = io.BytesIO()
            can = canvas.Canvas(packet, pagesize=page.mediaBox)
            
            can.setFillColorRGB(0.8, 0.8, 0.8, alpha=0.3)
            can.setFont("Helvetica", 50)
            
            width = float(page.mediaBox.getWidth())
            height = float(page.mediaBox.getHeight())
            
            can.saveState()
            can.translate(width/2, height/2)
            can.rotate(45)
            can.drawCentredString(0, 0, watermark_text)
            can.restoreState()
            
            can.save()
            
            packet.seek(0)
            watermark_pdf = PdfFileReader(packet)
            watermark_page = watermark_pdf.getPage(0)
            page.mergePage(watermark_page)
            
            pdf_writer.addPage(page)
        
        with open(output_path, 'wb') as out_pdf:
            pdf_writer.write(out_pdf)
        return output_path

    @staticmethod
    def extract_pages(input_path, output_path, pages):
        """提取指定页面"""
        pdf_reader = PdfFileReader(input_path)
        pdf_writer = PdfFileWriter()
        
        for page_num in pages:
            if 0 <= page_num - 1 < pdf_reader.getNumPages():
                pdf_writer.addPage(pdf_reader.getPage(page_num - 1))
        
        with open(output_path, 'wb') as out_pdf:
            pdf_writer.write(out_pdf)
        return output_path

    @staticmethod
    def merge_pdfs(pdf_paths, output_path):
        """合并多个PDF"""
        pdf_writer = PdfFileWriter()
        
        for path in pdf_paths:
            pdf_reader = PdfFileReader(path)
            for page_num in range(pdf_reader.getNumPages()):
                pdf_writer.addPage(pdf_reader.getPage(page_num))
        
        with open(output_path, 'wb') as out_pdf:
            pdf_writer.write(out_pdf)
        return output_path

    @staticmethod
    def get_page_count(pdf_path):
        """获取PDF页数"""
        with open(pdf_path, 'rb') as f:
            pdf = PdfFileReader(f)
            return pdf.getNumPages()

2.3 数据库模型 (app/models.py)

from datetime import datetime
from app import db
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))
    pdfs = db.relationship('PDFDocument', backref='owner', lazy='dynamic')

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

class PDFDocument(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    filename = db.Column(db.String(256))
    original_name = db.Column(db.String(256))
    filepath = db.Column(db.String(512))
    pages = db.Column(db.Integer)
    created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    
    def __repr__(self):
        return f'<PDFDocument {self.original_name}>'

2.4 主应用和路由 (app/init.py 和 app/routes.py)

# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
from config import Config

db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    db.init_app(app)
    migrate.init_app(app, db)
    login.init_app(app)

    from app.routes import main as main_blueprint
    app.register_blueprint(main_blueprint)

    from app.auth import bp as auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/auth')

    return app

from app import models

# app/routes.py
from flask import render_template, flash, redirect, url_for, request, send_from_directory, Blueprint
from flask_login import current_user, login_required
from werkzeug.utils import secure_filename
import os
from app import db
from app.models import PDFDocument
from app.utils.pdf_utils import PDFEditor
from config import Config

main = Blueprint('main', __name__)

@main.route('/')
@login_required
def index():
    pdfs = current_user.pdfs.order_by(PDFDocument.created_at.desc()).all()
    return render_template('index.html', pdfs=pdfs)

@main.route('/upload', methods=['POST'])
@login_required
def upload_pdf():
    if 'file' not in request.files:
        flash('No file selected')
        return redirect(url_for('main.index'))
    
    file = request.files['file']
    if file.filename == '':
        flash('No file selected')
        return redirect(url_for('main.index'))
    
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        filepath = os.path.join(Config.UPLOAD_FOLDER, filename)
        os.makedirs(os.path.dirname(filepath), exist_ok=True)
        file.save(filepath)
        
        # 获取页数
        page_count = PDFEditor.get_page_count(filepath)
        
        # 保存到数据库
        pdf = PDFDocument(
            filename=filename,
            original_name=file.filename,
            filepath=filepath,
            pages=page_count,
            owner=current_user
        )
        db.session.add(pdf)
        db.session.commit()
        
        flash('PDF uploaded successfully')
        return redirect(url_for('main.index'))
    
    flash('Invalid file type')
    return redirect(url_for('main.index'))

@main.route('/view/<int:pdf_id>')
@login_required
def view_pdf(pdf_id):
    pdf = PDFDocument.query.get_or_404(pdf_id)
    if pdf.owner != current_user:
        abort(403)
    return render_template('view.html', pdf=pdf)

@main.route('/edit/<int:pdf_id>', methods=['GET', 'POST'])
@login_required
def edit_pdf(pdf_id):
    pdf = PDFDocument.query.get_or_404(pdf_id)
    if pdf.owner != current_user:
        abort(403)
    
    if request.method == 'POST':
        operation = request.form.get('operation')
        output_filename = f"{operation}_{pdf.filename}"
        output_path = os.path.join(Config.PROCESSED_FOLDER, output_filename)
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        
        if operation == 'rotate':
            PDFEditor.rotate_pdf(pdf.filepath, output_path)
            flash('PDF rotated successfully')
        elif operation == 'watermark':
            watermark_text = request.form.get('watermark_text', 'WATERMARK')
            PDFEditor.add_watermark(pdf.filepath, output_path, watermark_text)
            flash('Watermark added successfully')
        elif operation == 'extract':
            pages = list(map(int, request.form.get('pages', '').split(',')))
            PDFEditor.extract_pages(pdf.filepath, output_path, pages)
            flash('Pages extracted successfully')
        
        # 保存新版本到数据库
        new_pdf = PDFDocument(
            filename=output_filename,
            original_name=f"{operation}_{pdf.original_name}",
            filepath=output_path,
            pages=PDFEditor.get_page_count(output_path),
            owner=current_user
        )
        db.session.add(new_pdf)
        db.session.commit()
        
        return redirect(url_for('main.view_pdf', pdf_id=new_pdf.id))
    
    return render_template('edit.html', pdf=pdf)

@main.route('/delete/<int:pdf_id>', methods=['POST'])
@login_required
def delete_pdf(pdf_id):
    pdf = PDFDocument.query.get_or_404(pdf_id)
    if pdf.owner != current_user:
        abort(403)
    
    try:
        os.remove(pdf.filepath)
    except OSError:
        pass
    
    db.session.delete(pdf)
    db.session.commit()
    flash('PDF deleted successfully')
    return redirect(url_for('main.index'))

@main.route('/download/<int:pdf_id>')
@login_required
def download_pdf(pdf_id):
    pdf = PDFDocument.query.get_or_404(pdf_id)
    if pdf.owner != current_user:
        abort(403)
    return send_from_directory(os.path.dirname(pdf.filepath), os.path.basename(pdf.filepath), as_attachment=True)

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in Config.ALLOWED_EXTENSIONS

3. 前端实现

3.1 基础模板 (templates/base.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}PDF Manager{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
</head>
<body>
    <nav class="navbar">
        <div class="container">
            <a href="{{ url_for('main.index') }}" class="logo">PDF Manager</a>
            <div class="nav-links">
                {% if current_user.is_authenticated %}
                    <a href="{{ url_for('main.index') }}">My PDFs</a>
                    <a href="{{ url_for('auth.logout') }}">Logout</a>
                {% else %}
                    <a href="{{ url_for('auth.login') }}">Login</a>
                    <a href="{{ url_for('auth.register') }}">Register</a>
                {% endif %}
            </div>
        </div>
    </nav>

    <div class="container">
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                {% for category, message in messages %}
                    <div class="alert alert-{{ category }}">
                        {{ message }}
                    </div>
                {% endfor %}
            {% endif %}
        {% endwith %}

        {% block content %}{% endblock %}
    </div>

    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
    {% block scripts %}{% endblock %}
</body>
</html>

3.2 主页模板 (templates/index.html)

{% extends "base.html" %}

{% block content %}
<div class="pdf-manager">
    <h1>My PDF Documents</h1>
    
    <div class="upload-section">
        <h2>Upload PDF</h2>
        <form action="{{ url_for('main.upload_pdf') }}" method="POST" enctype="multipart/form-data" class="upload-form">
            <div class="form-group">
                <input type="file" name="file" id="file" accept=".pdf" required>
                <label for="file" class="file-label">
                    <i class="fas fa-file-pdf"></i>
                    <span>Choose a PDF file</span>
                </label>
            </div>
            <button type="submit" class="btn">Upload</button>
        </form>
    </div>
    
    <div class="pdf-list">
        <h2>Your Documents</h2>
        {% if pdfs %}
            <table class="pdf-table">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Pages</th>
                        <th>Uploaded</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody>
                    {% for pdf in pdfs %}
                        <tr>
                            <td>{{ pdf.original_name }}</td>
                            <td>{{ pdf.pages }}</td>
                            <td>{{ pdf.created_at.strftime('%Y-%m-%d') }}</td>
                            <td class="actions">
                                <a href="{{ url_for('main.view_pdf', pdf_id=pdf.id) }}" class="btn btn-sm btn-view">
                                    <i class="fas fa-eye"></i> View
                                </a>
                                <a href="{{ url_for('main.edit_pdf', pdf_id=pdf.id) }}" class="btn btn-sm btn-edit">
                                    <i class="fas fa-edit"></i> Edit
                                </a>
                                <a href="{{ url_for('main.download_pdf', pdf_id=pdf.id) }}" class="btn btn-sm btn-download">
                                    <i class="fas fa-download"></i> Download
                                </a>
                                <form action="{{ url_for('main.delete_pdf', pdf_id=pdf.id) }}" method="POST" class="delete-form">
                                    <button type="submit" class="btn btn-sm btn-delete" onclick="return confirm('Are you sure?')">
                                        <i class="fas fa-trash"></i> Delete
                                    </button>
                                </form>
                            </td>
                        </tr>
                    {% endfor %}
                </tbody>
            </table>
        {% else %}
            <p>No PDF documents uploaded yet.</p>
        {% endif %}
    </div>
</div>
{% endblock %}

3.3 查看PDF模板 (templates/view.html)

{% extends "base.html" %}

{% block content %}
<div class="pdf-viewer">
    <h1>{{ pdf.original_name }}</h1>
    
    <div class="pdf-info">
        <p><strong>Pages:</strong> {{ pdf.pages }}</p>
        <p><strong>Uploaded:</strong> {{ pdf.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
    </div>
    
    <div class="pdf-actions">
        <a href="{{ url_for('main.edit_pdf', pdf_id=pdf.id) }}" class="btn btn-edit">
            <i class="fas fa-edit"></i> Edit PDF
        </a>
        <a href="{{ url_for('main.download_pdf', pdf_id=pdf.id) }}" class="btn btn-download">
            <i class="fas fa-download"></i> Download
        </a>
        <form action="{{ url_for('main.delete_pdf', pdf_id=pdf.id) }}" method="POST" class="delete-form">
            <button type="submit" class="btn btn-delete" onclick="return confirm('Are you sure?')">
                <i class="fas fa-trash"></i> Delete
            </button>
        </form>
    </div>
    
    <div class="pdf-preview">
        <iframe src="{{ url_for('main.download_pdf', pdf_id=pdf.id) }}" width="100%" height="600px">
            Your browser does not support PDF preview. Please download the PDF to view it.
        </iframe>
    </div>
</div>
{% endblock %}

3.4 编辑PDF模板 (templates/edit.html)

{% extends "base.html" %}

{% block content %}
<div class="pdf-editor">
    <h1>Edit PDF: {{ pdf.original_name }}</h1>
    
    <div class="editor-options">
        <form method="POST" action="{{ url_for('main.edit_pdf', pdf_id=pdf.id) }}">
            <div class="form-group">
                <label>Select Operation:</label>
                <div class="radio-group">
                    <label class="radio-option">
                        <input type="radio" name="operation" value="rotate" checked>
                        <div class="radio-content">
                            <i class="fas fa-redo"></i>
                            <span>Rotate 90°</span>
                        </div>
                    </label>
                    
                    <label class="radio-option">
                        <input type="radio" name="operation" value="watermark">
                        <div class="radio-content">
                            <i class="fas fa-stamp"></i>
                            <span>Add Watermark</span>
                        </div>
                    </label>
                    
                    <label class="radio-option">
                        <input type="radio" name="operation" value="extract">
                        <div class="radio-content">
                            <i class="fas fa-cut"></i>
                            <span>Extract Pages</span>
                        </div>
                    </label>
                </div>
            </div>
            
            <div id="watermark-options" class="options-panel" style="display: none;">
                <div class="form-group">
                    <label for="watermark_text">Watermark Text:</label>
                    <input type="text" id="watermark_text" name="watermark_text" value="WATERMARK">
                </div>
            </div>
            
            <div id="extract-options" class="options-panel" style="display: none;">
                <div class="form-group">
                    <label for="pages">Pages to Extract (e.g., 1,3,5 or 1-5):</label>
                    <input type="text" id="pages" name="pages" placeholder="1,2,3 or 1-5">
                </div>
            </div>
            
            <div class="form-actions">
                <button type="submit" class="btn btn-primary">
                    <i class="fas fa-save"></i> Apply Changes
                </button>
                <a href="{{ url_for('main.view_pdf', pdf_id=pdf.id) }}" class="btn btn-cancel">
                    Cancel
                </a>
            </div>
        </form>
    </div>
    
    <div class="pdf-preview">
        <iframe src="{{ url_for('main.download_pdf', pdf_id=pdf.id) }}" width="100%" height="600px">
            Your browser does not support PDF preview. Please download the PDF to view it.
        </iframe>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
    const operationRadios = document.querySelectorAll('input[name="operation"]');
    const optionsPanels = document.querySelectorAll('.options-panel');
    
    operationRadios.forEach(radio => {
        radio.addEventListener('change', function() {
            // Hide all options panels
            optionsPanels.forEach(panel => {
                panel.style.display = 'none';
            });
            
            // Show selected panel
            const selectedPanel = document.getElementById(this.value + '-options');
            if (selectedPanel) {
                selectedPanel.style.display = 'block';
            }
        });
    });
    
    // Trigger change event for initially checked radio
    document.querySelector('input[name="operation"]:checked').dispatchEvent(new Event('change'));
});
</script>
{% endblock %}

3.5 主样式文件 (static/css/main.css)

/* Base Styles */
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.6;
    margin: 0;
    padding: 0;
    background-color: #f8f9fa;
    color: #333;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}

/* Navigation */
.navbar {
    background-color: #343a40;
    color: white;
    padding: 15px 0;
}

.navbar .container {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.logo {
    color: white;
    font-size: 1.5rem;
    font-weight: bold;
    text-decoration: none;
}

.nav-links a {
    color: white;
    text-decoration: none;
    margin-left: 20px;
}

.nav-links a:hover {
    color: #adb5bd;
}

/* Alerts */
.alert {
    padding: 10px 15px;
    margin-bottom: 20px;
    border-radius: 4px;
}

.alert-success {
    background-color: #d4edda;
    color: #155724;
}

.alert-danger {
    background-color: #f8d7da;
    color: #721c24;
}

/* Buttons */
.btn {
    display: inline-block;
    padding: 8px 16px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    text-decoration: none;
    font-size: 14px;
    transition: background-color 0.3s;
}

.btn-primary {
    background-color: #007bff;
    color: white;
}

.btn-primary:hover {
    background-color: #0069d9;
}

.btn-sm {
    padding: 5px 10px;
    font-size: 12px;
}

.btn-view {
    background-color: #17a2b8;
    color: white;
}

.btn-edit {
    background-color: #ffc107;
    color: #212529;
}

.btn-download {
    background-color: #28a745;
    color: white;
}

.btn-delete {
    background-color: #dc3545;
    color: white;
}

.btn-cancel {
    background-color: #6c757d;
    color: white;
}

/* Forms */
.form-group {
    margin-bottom: 20px;
}

.form-group label {
    display: block;
    margin-bottom: 5px;
    font-weight: 600;
}

.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="email"],
.form-group input[type="number"],
.form-group input[type="file"],
.form-group select,
.form-group textarea {
    width: 100%;
    padding: 8px 12px;
    border: 1px solid #ced4da;
    border-radius: 4px;
    font-size: 14px;
}

.file-label {
    display: block;
    padding: 12px;
    border: 2px dashed #ced4da;
    border-radius: 4px;
    text-align: center;
    cursor: pointer;
    transition: border-color 0.3s;
}

.file-label:hover {
    border-color: #adb5bd;
}

.file-label i {
    font-size: 24px;
    display: block;
    margin-bottom: 5px;
    color: #dc3545;
}

/* PDF List */
.pdf-list {
    margin-top: 30px;
}

.pdf-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 20px;
}

.pdf-table th, .pdf-table td {
    padding: 12px 15px;
    text-align: left;
    border-bottom: 1px solid #dee2e6;
}

.pdf-table th {
    background-color: #f8f9fa;
    font-weight: 600;
}

.actions {
    display: flex;
    gap: 5px;
    flex-wrap: wrap;
}

.delete-form {
    display: inline;
}

/* PDF Viewer */
.pdf-viewer, .pdf-editor {
    margin-top: 20px;
}

.pdf-info {
    background-color: #f8f9fa;
    padding: 15px;
    border-radius: 4px;
    margin-bottom: 20px;
}

.pdf-actions {
    display: flex;
    gap: 10px;
    margin-bottom: 20px;
}

.pdf-preview {
    border: 1px solid #dee2e6;
    border-radius: 4px;
    overflow: hidden;
}

/* Editor Options */
.editor-options {
    background-color: white;
    padding: 20px;
    border-radius: 4px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    margin-bottom: 20px;
}

.radio-group {
    display: flex;
    gap: 15px;
    margin-bottom: 20px;
}

.radio-option {
    flex: 1;
}

.radio-option input[type="radio"] {
    display: none;
}

.radio-content {
    padding: 15px;
    border: 2px solid #dee2e6;
    border-radius: 4px;
    text-align: center;
    cursor: pointer;
    transition: all 0.3s;
}

.radio-option input[type="radio"]:checked + .radio-content {
    border-color: #007bff;
    background-color: #e7f3ff;
}

.radio-content i {
    font-size: 24px;
    display: block;
    margin-bottom: 5px;
    color: #007bff;
}

.options-panel {
    padding: 15px;
    background-color: #f8f9fa;
    border-radius: 4px;
    margin-bottom: 20px;
}

.form-actions {
    display: flex;
    gap: 10px;
}

/* Responsive */
@media (max-width: 768px) {
    .radio-group {
        flex-direction: column;
    }
    
    .pdf-actions {
        flex-direction: column;
    }
    
    .actions {
        flex-direction: column;
    }
}

4. 认证系统

用户认证系统代码:

# app/auth.py
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required
from app import db
from app.models import User
from app.forms import LoginForm, RegistrationForm

bp = Blueprint('auth', __name__)

@bp.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('auth.login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('main.index'))
    return render_template('auth/login.html', title='Sign In', form=form)

@bp.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('main.index'))

@bp.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('Congratulations, you are now a registered user!')
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html', title='Register', form=form)

5. 运行说明

1、安装依赖:

 pip install flask flask-sqlalchemy flask-login flask-migrate PyPDF2 reportlab python-dotenv

2、初始化数据库:

flask db init
flask db migrate
flask db upgrade

3、运行应用:

flask run

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值