1、需求:
公司文档涉及PDF类型的比较多,需要PDF编辑内容,并且可批量定制化满足业务灵活自主的自动化处理功能。
2、产品设计
提供一个完整的全栈解决方案,包含前端界面和后端服务,实现PDF文档的增删改查(CRUD)和编辑保存功能。
-
完整的CRUD功能:
-
创建:上传PDF文件
-
读取:查看PDF列表和详情
-
更新:编辑PDF(旋转、水印、提取页面)
-
删除:删除PDF文件
-
-
用户认证:
-
注册、登录、注销功能
-
用户只能访问自己的PDF文件
-
-
PDF编辑功能:
-
旋转PDF页面
-
添加水印
-
提取特定页面
-
下载处理后的文件
-
-
响应式设计:
-
适配不同屏幕尺寸
-
现代化的用户界面
-
-
安全特性:
-
文件类型验证
-
文件安全存储
-
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_dotenvload_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 ioclass 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 UserMixinclass 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 Configdb = 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 Configmain = 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, RegistrationFormbp = 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
71万+

被折叠的 条评论
为什么被折叠?



