【Flask】实现一个前后端一体的项目-脚手架
目录
在前面的文章中我们都是实现Flask=vue2.x的前后端分离框架,那有没有前后端不分离的框架,前后端分离,对于大多数没有前端基础的,可能维护起来有点费力,涉及的知识点相对多一点,那么本篇将带你完整实现一个前后端一体的项目框架架构,包括实现用户的登录,登出,界面功能菜单显示,完整的一个后台管理系统,之后你就可以在系统中陆续实现你自己的功能
- 实现Flask项目创建
- 实现登录功能
- 创建数据库,生成数据库表
- 实现页面功能
- 实现基础页面的展示布局
项目背景介绍
我们将基于Flask框架,实现一个完整的前后端一体的项目后台管理系统,实现简单的个人中心,,一个完整的小型项目框架架构
注意:这种前后端一体项目,只适合你永远自己实现个小项目,大项目还是使用前后端分离这种实现方式,切记这种方式不要在大型项目中尝试
概述
首先如果你要能使用前后端一体的Flask框架,前置条件你需要先具备
技术知识储备:
- 1、熟悉Python基础语法
- 2、熟悉Flask web框架-可参考下面链接https://dormousehole.readthedocs.io/en/latest/tutorial/index.html
- 3、熟悉peewee ORM框架-参考下面链接
- https://geek-docs.com/python/python-tutorial/python-peewee.html
- 4、有一定的前端知识,了解Jinja语法,方便前后端传参
- 5、需要有数据库的功底
先在本地创建一个Flask项目,Flask项目创建后就如下面这种效果
- 默认Jinja模版
启动验证下项目是不是正常
浏览器访问一下地址:127.0.0.1:5000
正常可以访问,没有任何问题
项目概述
本教程将基于Flask框架,实现一个完整的前后端一体的项目,带你完整实现第一个前后端一体的完整项目和功能实现过程
数据库创建
数据库是必不可少的,在你本地先安装好数据库,这里我本地使用的是mysql,我已经安装好了,如果你没有安装,你可以先把数据库安装好,并且启用数据库连接好数据库,创建一个数据库
这里数据库的名字按照你自己命名即可,这里我命名如下(名字你随意就行)
项目结构
略
数据库模型
接下来需要创建对应的数据模型,生成执行后生成对应的数据表,这里我们采用peewee ORM的方式生成数据库表
注意:peewee 不会自动创建数据库,需要自己先在本地吧数据库创建好
建立数据库模型
首先创建一个models包,用于存储各种模型文件
创建config文件,用于存储数据连接配置文件信息,填写上面我们创建的数据库信息
config文件中完整代码
import osbasedir = os.path.abspath(os.path.dirname(__file__))class Config:SECRET_KEY = os.environ.get('SECRET_KEY') or 'super-secret'DB_HOST = 'localhost'DB_PORT = 3306DB_USER = 'root'DB_PASSWD = 'Rebort!123'DB_DATABASE = 'luntanrebort'ITEMS_PER_PAGE = 10JWT_AUTH_URL_RULE = '/api/auth'@staticmethoddef init_app(app):passclass DevelopmentConfig(Config):DEBUG = Trueclass TestingConfig(Config):TESTING = Trueclass ProductionConfig(Config):PRODUCTION = Trueconfig = {'development': DevelopmentConfig,'testing': TestingConfig,'production': ProductionConfig,'default': DevelopmentConfig
}
创建基础模型,所有模型继承此模型
class BaseModel(Model):class Meta:database = dbdef __str__(self):r = {}for k in self._data.keys():try:r[k] = str(getattr(self, k))except:r[k] = json.dumps(getattr(self, k))return json.dumps(r, ensure_ascii=False)
用户模型
# 用户模型
class User(UserMixin, BaseModel):id = IntegerField(primary_key=True) # 添加主键username = CharField(max_length=80, unique=True, null=False) # Peewee 用 null=False 代替 nullable=Falsepassword_hash = CharField(max_length=128, null=False) # 字段名统一为 password_hashemail = CharField(max_length=120, unique=True, null=False)fullname = CharField(max_length=50, null=True) # 真实性名(允许为空)phone = CharField(max_length=20, null=True) # 电话(允许为空)register_time = DateTimeField(default=datetime.datetime.now)last_login_time = DateTimeField(null=True)failed_login_count = IntegerField(default=0)locked = BooleanField(default=False)avatar_url = CharField(max_length=200, default='default.png') # Peewee 不需要 _ 前缀映射signature = TextField(default="这个人很懒,还没写个性签名~")is_admin = BooleanField(default=False)status = BooleanField(default=True) # 生效失效标识def verify_password(self, raw_password):return check_password_hash(self.password_hash, raw_password)
通知数据模型
# 通知人配置
class CfgNotify(BaseModel):id = IntegerField(primary_key=True) # 添加主键check_order = IntegerField() # 排序notify_type = CharField(max_length=10) # 通知类型:MAIL/SMSnotify_name = CharField(max_length=50) # 通知人姓名notify_number = CharField(max_length=100) # 通知号码status = BooleanField(default=True) # 生效失效标识
贴子模型
# 帖子模型
class Post(BaseModel):id = IntegerField(primary_key=True)title = CharField(max_length=100, null=False)content = TextField(null=False)time = DateTimeField(default=datetime.datetime.now)author_id = ForeignKeyField(User, backref='posts') likes = IntegerField(default=0)views = IntegerField(default=0)
评论模型
# 评论模型
class Comment(BaseModel):id = IntegerField(primary_key=True)content = TextField(null=False)time = DateTimeField(default=datetime.datetime.now)author_id = ForeignKeyField(User, backref='comments')post_id = ForeignKeyField(Post, backref='comments', on_delete='CASCADE')
收藏模型
# 收藏模型
class Favorite(BaseModel):id = IntegerField(primary_key=True)user_id = ForeignKeyField(User, backref='favorites')post_id = ForeignKeyField(Post, backref='favorited_by')time = DateTimeField(default=datetime.datetime.now)class Meta:indexes = ((('user_id', 'post_id'), True), )
点赞模型
# 点赞模型
class Like(BaseModel):id = IntegerField(primary_key=True)user_id = ForeignKeyField(User, backref='likes')post_id = ForeignKeyField(Post, backref='post_likes')time = DateTimeField(default=datetime.datetime.now)class Meta:indexes = ((('user_id', 'post_id'), True),)
model模型的完整代码如下:
# -*- coding: utf-8 -*-
import os
import datetime
import json
from werkzeug.security import check_password_hash
from flask_login import UserMixin
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))from configs.config import config
cfg = config[os.getenv('FLASK_CONFIG') or 'default']
# Peewee 相关导入
from peewee import (Model, CharField, BooleanField,IntegerField, TextField, DateTimeField, ForeignKeyField)
from playhouse.pool import PooledMySQLDatabase
from playhouse.shortcuts import ReconnectMixin# 配置导入(确保 conf/config.py 存在)
from configs.config import config# 初始化配置
cfg = config[os.getenv('FLASK_CONFIG') or 'default']# ------------------------------
# 1. 修复数据库连接
# ------------------------------
class RetryMySQLDatabase(ReconnectMixin, PooledMySQLDatabase):_instance = None@staticmethoddef get_db_instance():if not RetryMySQLDatabase._instance:RetryMySQLDatabase._instance = RetryMySQLDatabase(database=cfg.DB_NAME, host=cfg.DB_HOST,user=cfg.DB_USER,password=cfg.DB_PASSWD,port=cfg.DB_PORT,charset='utf8mb4', # 添加字符集,避免中文乱码max_connections=32,stale_timeout=300)return RetryMySQLDatabase._instance# 初始化数据库连接
db = RetryMySQLDatabase.get_db_instance()# ------------------------------
# 2. 基础模型类(所有模型继承此类)
# ------------------------------
class BaseModel(Model):class Meta:database = dbdef __str__(self):r = {}for k in self._data.keys():try:r[k] = str(getattr(self, k))except:r[k] = json.dumps(getattr(self, k))return json.dumps(r, ensure_ascii=False)# ------------------------------
# 3. 模型定义(统一使用 Peewee 语法)
# ------------------------------# 用户模型(仅一个 User 类,继承 BaseModel)
class User(UserMixin, BaseModel):id = IntegerField(primary_key=True) # 添加主键username = CharField(max_length=80, unique=True, null=False) # Peewee 用 null=False 代替 nullable=Falsepassword_hash = CharField(max_length=128, null=False) # 字段名统一为 password_hashemail = CharField(max_length=120, unique=True, null=False)fullname = CharField(max_length=50, null=True) # 真实性名(允许为空)phone = CharField(max_length=20, null=True) # 电话(允许为空)register_time = DateTimeField(default=datetime.datetime.now)last_login_time = DateTimeField(null=True)failed_login_count = IntegerField(default=0)locked = BooleanField(default=False)avatar_url = CharField(max_length=200, default='default.png') # Peewee 不需要 _ 前缀映射signature = TextField(default="这个人很懒,还没写个性签名~")is_admin = BooleanField(default=False)status = BooleanField(default=True) # 生效失效标识def verify_password(self, raw_password):return check_password_hash(self.password_hash, raw_password)# 通知人配置
class CfgNotify(BaseModel):id = IntegerField(primary_key=True) # 添加主键check_order = IntegerField() # 排序notify_type = CharField(max_length=10) # 通知类型:MAIL/SMSnotify_name = CharField(max_length=50) # 通知人姓名notify_number = CharField(max_length=100) # 通知号码status = BooleanField(default=True) # 生效失效标识# 帖子模型
class Post(BaseModel):id = IntegerField(primary_key=True)title = CharField(max_length=100, null=False)content = TextField(null=False)time = DateTimeField(default=datetime.datetime.now)author_id = ForeignKeyField(User, backref='posts') likes = IntegerField(default=0)views = IntegerField(default=0)# 评论模型
class Comment(BaseModel):id = IntegerField(primary_key=True)content = TextField(null=False)time = DateTimeField(default=datetime.datetime.now)author_id = ForeignKeyField(User, backref='comments')post_id = ForeignKeyField(Post, backref='comments', on_delete='CASCADE') # 级联删除# 收藏模型
class Favorite(BaseModel):id = IntegerField(primary_key=True)user_id = ForeignKeyField(User, backref='favorites')post_id = ForeignKeyField(Post, backref='favorited_by')time = DateTimeField(default=datetime.datetime.now)class Meta:indexes = ((('user_id', 'post_id'), True), # 联合唯一索引:一个用户只能收藏一个帖子一次)# 点赞模型
class Like(BaseModel):id = IntegerField(primary_key=True)user_id = ForeignKeyField(User, backref='likes')post_id = ForeignKeyField(Post, backref='post_likes')time = DateTimeField(default=datetime.datetime.now)class Meta:indexes = ((('user_id', 'post_id'), True), # 联合唯一索引:一个用户只能点赞一个帖子一次)# ------------------------------
# 4. 用户加载函数(需在 User 模型定义后)
# ------------------------------
# 注意:login_manager 需要从 Flask 应用初始化中导入
# 此处先注释,在 Flask 应用初始化文件中配置(见下方说明)
"""
@login_manager.user_loader
def load_user(user_id):try:return User.get(User.id == int(user_id))except User.DoesNotExist:return None
"""# ------------------------------
# 5.建表函数
# ------------------------------
def create_tables():# 按顺序创建表(外键依赖的表需后创建)tables = [User, CfgNotify, Post, Comment, Favorite, Like]with db.connection_context(): # 使用上下文管理器确保连接正确db.create_tables(tables, safe=True) # safe=True:已存在的表不会报错print(f"成功创建 {len(tables)} 张表!")# ------------------------------
# 6. 执行建表(调用 create_tables())
# ------------------------------
if __name__ == '__main__':create_tables() # 执行建表
由于这里我的入口不是app.py启动项目,此时将启动入口放在了manage.py中,使用Flask CLI的方式进行创建数据表
为什么 flask create-tables
需要 FLASK_APP
?
flask
命令本质是 Flask 官方 CLI 工具,它需要知道哪个文件定义了 Flask 应用实例(即 app = Flask(__name__)
或 create_app()
)。
- 若不设置
FLASK_APP
,Flask 会自动搜索当前目录下的app.py
/wsgi.py
等文件,但如果您的应用入口是manage.py
,则必须显式指定。 flask --app manage.py create-tables
中的--app manage.py
本质是临时指定FLASK_APP
,与设置环境变量作用相同。
在项目根目录下创建文件.flaskenv
文件中写入数据如下
FLASK_APP=manage.py # 告诉 Flask 应用入口是 manage.py
FLASK_CONFIG=development # 可选:默认环境
需要安装下环境
pip install python-dotenv
manage.py中的完整代码
# manage.py
import os
from flask import Flask
from apis import create_app # 确保 create_app 能正确返回 Flask 实例
from models.model import create_tables # 确保模型导入无错误# 1. 初始化 Flask 应用(必须在命令注册前执行)
app = create_app(os.getenv('FLASK_CONFIG') or 'default')# 2. 关键:注册 CLI 命令(必须在 app 初始化后)
@app.cli.command("create-tables") # 命令名:create-tables
def cli_create_tables():"""创建所有数据库表"""create_tables()print("✅ 数据库表创建成功!")# 3. 其他命令(可选,保持原有功能)
@app.cli.command("runserver")
def cli_runserver():"""启动开发服务器"""app.run(host="0.0.0.0", port=5000, debug=True)@app.cli.command("test")
def cli_test():"""运行测试"""import unittesttests = unittest.TestLoader().discover('tests')unittest.TextTestRunner(verbosity=2).run(tests)# 4. 若直接运行 manage.py,提示使用 Flask CLI
if __name__ == '__main__':print("请使用 Flask CLI 命令:")print("flask create-tables # 创建表")print("flask runserver # 启动服务器")
执行创建数据表的语句
flask create-tables
查看此时数据库中是不是已经有创建的数据表
表创建成功,如果你看到的和我的一样,那么此时数据库表第一步实现成功
接下来需要创建一个管理员,用于登录系统,我们直接在manage中写一个创建管理员的方法
@app.cli.command("create-admin")
def create_admin():from models.model import Userfrom werkzeug.security import generate_password_hashimport datetimeif User.select().where(User.username == 'admin').exists():print("⚠️ 管理员已存在!")return# 模型自动处理所有默认值字段(failed_login_count/locked/avatar_url 等)User.create(id=1,username='admin',password_hash=generate_password_hash('admin'), # 替换为实际密码fullname='管理员',email='admin@admin.com',phone='156341234',status=True,is_admin=True # 显式指定管理员权限(模型默认是 False)# 其他字段(register_time/failed_login_count 等)无需手动指定,模型自动填充)print("✅ 管理员创建成功!")
执行创建命令
flask create-admin
也可以直接通过SQL插入(默认用户名和密码都是admin/admin)
INSERT INTO `user` (`id`, `username`, `password`, `fullname`, `email`, `phone`, `status`)
VALUES(1, 'admin', 'pbkdf2:sha1:1000$Km1vdx3W$9aa07d3b79ab88aae53e45d26d0d4d4e097a6cd3', '管理员', 'admin@admin.com', '15685878475', 1);
编写登录页面路由
from flask import render_template, redirect, request, url_for, flash
from . import auth
from .forms import LoginForm
from app.models import User
from flask_login import login_user, logout_user, login_required@auth.route('/login', methods=['GET', 'POST'])
def login():form = LoginForm()print(form.rememberme.data)if form.validate_on_submit():try:user = User.get(User.username == form.username.data)if user.verify_password(form.password.data):login_user(user, form.rememberme.data)return redirect(request.args.get('next') or url_for('main.index'))else:flash('用户名或密码错误')except:flash('用户名或密码错误')return render_template('auth/login.html', form=form)@auth.route('/logout')
@login_required
def logout():logout_user()flash('您已退出登录')return redirect(url_for('auth.login'))
登录时增加一个记住用户账号的功能
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Lengthclass LoginForm(FlaskForm):username = StringField('用户名', validators=[DataRequired(), Length(1, 64), ])password = PasswordField('密码', validators=[DataRequired()])rememberme = BooleanField('记住我')submit = SubmitField('提交')
编写登录页面的HTML
login.html
登录页面完整代码如下
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"><title>管理员REBORT后台</title><link rel="stylesheet" href="{{ url_for('static', filename='plugins/bootstrap/css/bootstrap.min.css') }}"><link rel="stylesheet" href="{{ url_for('static', filename='plugins/iCheck/square/blue.css') }}"><link rel="stylesheet" href="{{ url_for('static', filename='plugins/pace/pace.min.css') }}"><link rel="stylesheet" href="{{ url_for('static', filename='plugins/adminlte/css/AdminLTE.min.css') }}"><link rel="stylesheet" href="{{ url_for('static', filename='plugins/adminlte/css/skins/skin-blue.min.css') }}"><link rel="stylesheet" href="{{ url_for('static', filename='css/global.css') }}"><link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"><style>/* 全局样式 */body {/* 替换为您的图片URL,确保图片路径正确 */background-image: url("{{ url_for('static', filename='images/rebort.jpg') }}");background-size: cover; /* 图片覆盖全屏 */background-position: center; /* 图片居中 */background-repeat: no-repeat; /* 不重复平铺 */min-height: 100vh;display: flex;flex-direction: column;align-items: center;justify-content: center;font-family: 'Segoe UI', 'Roboto', sans-serif;padding: 20px;/* 如果图片较亮,可添加半透明遮罩增加文字可读性 */position: relative;}/* 添加半透明遮罩(可选) */body::before {content: "";position: absolute;top: 0;left: 0;width: 100%;height: 100%;background: rgba(255, 255, 255, 0.3); /* 白色遮罩,透明度80% */z-index: -1; /* 确保遮罩在内容下方 */}/* 登录框容器 */.login-container {width: 100%;max-width: 420px;perspective: 1000px;}/* 登录卡片 */.login-card {background: #ffffff;border-radius: 12px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);overflow: hidden;transition: all 0.3s ease;transform-style: preserve-3d;transform: translateY(0);}.login-card:hover {box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);transform: translateY(-5px);}/* 登录头部 */.login-header {background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);color: white;padding: 25px;text-align: center;position: relative;overflow: hidden;}.login-header h1 {font-size: 24px;font-weight: 600;margin: 0;position: relative;z-index: 2;}.login-header::before {content: "";position: absolute;top: 0;left: 0;width: 100%;height: 100%;background: rgba(255, 255, 255, 0.1);transform: skewY(-3deg);transform-origin: top right;z-index: 1;}/* 登录主体 */.login-body {padding: 30px;}.login-message {color: #666;margin-bottom: 25px;text-align: center;font-size: 15px;}/* 表单样式 */.form-group {margin-bottom: 20px;position: relative;}.form-control {height: 50px;padding: 10px 15px 10px 45px;border: 1px solid #e1e5eb;border-radius: 8px;font-size: 15px;transition: all 0.3s ease;box-shadow: none !important;}.form-control:focus {border-color: #3498db;box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15) !important;}.form-control-icon {position: absolute;left: 15px;top: 50%;transform: translateY(-50%);color: #a0a6ac;font-size: 18px;}/* 提醒消息 */.alert {border-radius: 8px;margin-bottom: 20px;padding: 12px 15px;font-size: 14px;animation: fadeIn 0.3s ease;}/* 记住我 */.remember-me {display: flex;align-items: center;margin-bottom: 20px;}.remember-me input {margin-right: 8px;}.remember-me label {color: #555;font-size: 14px;cursor: pointer;}/* 按钮样式 */.btn-primary {background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);border: none;border-radius: 8px;height: 50px;font-size: 16px;font-weight: 500;transition: all 0.3s ease;box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);}.btn-primary:hover, .btn-primary:focus {background: linear-gradient(135deg, #2980b9 0%, #3498db 100%);transform: translateY(-2px);box-shadow: 0 6px 15px rgba(52, 152, 219, 0.35);}.btn-primary:active {transform: translateY(0);}/* iCheck 样式调整 */.icheckbox_square-blue {margin-top: -2px;}.icheckbox_square-blue.checked {background-color: #3498db;border-color: #3498db;}.icheckbox_square-blue.checked::after {border-color: white;}/* 加载动画 */@keyframes fadeIn {from { opacity: 0; transform: translateY(10px); }to { opacity: 1; transform: translateY(0); }}/* 响应式调整 */@media (max-width: 576px) {.login-body {padding: 25px 20px;}.form-control, .btn-primary {height: 46px;}}</style><!--[if lt IE 9]><script src="plugins/html5shiv.min.js"></script><script src="plugins/respond.min.js"></script><![endif]-->
</head>
<body><div class="login-container"><div class="login-card"><div class="login-header"><h1>使用管理员密码登录</h1></div><div class="login-body"><p class="login-message">请输入管理员帐户及密码</p><form method="post">{{form.hidden_tag()}}<div class="form-group"><i class="fa fa-user form-control-icon"></i>{{form.username(class_="form-control",placeholder="用户名")}}</div><div class="form-group"><i class="fa fa-lock form-control-icon"></i>{{form.password(class_="form-control",placeholder="密码")}}</div>{% for message in get_flashed_messages() %}<div class="alert alert-warning"><button type="button" class="close" data-dismiss="alert">×</button>{{ message }}</div>{% endfor %}<div class="remember-me"><div class="checkbox icheck"><label>{{form.rememberme()}}{{form.rememberme.label}}</label></div></div>{{form.submit(class_="btn btn-primary btn-block")}}</form></div></div></div><script src="{{ url_for('static', filename='plugins/jQuery/jquery-2.2.3.min.js') }}"></script><script src="{{ url_for('static', filename='plugins/bootstrap/js/bootstrap.min.js') }}"></script><script src="{{ url_for('static', filename='plugins/iCheck/icheck.min.js') }}"></script><script src="{{ url_for('static', filename='plugins/pace/pace.min.js') }}"></script><script src="{{ url_for('static', filename='js/global.js') }}"></script><script>function initPage() {// 初始化iCheck插件$('input').iCheck({checkboxClass: 'icheckbox_square-blue',radioClass: 'iradio_square-blue',increaseArea: '20%'});// 表单字段聚焦效果$('.form-control').on('focus', function() {$(this).parent().find('.form-control-icon').css('color', '#3498db');}).on('blur', function() {$(this).parent().find('.form-control-icon').css('color', '#a0a6ac');});// 添加页面载入动画Pace.on('done', function() {$('.login-card').addClass('loaded');});}</script>
</body>
</html>
此时启动后端项目,查看此时登录界面是不是正常显示成功
登录页展示成功,至此,登录功能已全部实现
接着陆续实现其他的功能页面
这里由于功能页面太多,这里就不一一贴出每个页面的代码了,最终实现后的效果如下
登录后页面展示如下
至此,就完成了一个前后端一体的脚手架管理后台