【Pepper机器人管家(二)】项目组成-Web开发篇:后端

Web模块的大致结构是基于MVC架构的flask+jinja2+mysql的前后端不分离的结构

项目概述

Web模块的大致结构是基于MVC架构的flask+jinja2+mysql的前后端不分离的结构,大致结构已经在上篇中介绍了,这里不做赘述了。由于项目需求比较简单而且需求特殊,后端选择自己构建导致有些用法并不完全满足falsk的开发规范。前端没有使用框架,而是使用开源项目二次开发,代码中有许多冗余。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
│  README.md
│  .env //环境变量
│  app.log //运行日志
│  runserver.py //项目启动文件
│  requirements.txt //项目依赖
├─app
│  │  config.py //配置文件,数据库连接(需要在环境变量中加入此路径)
│  │  forms.py //验证表单程序
│  │  __init__.py //模块初始化文件,Flask 程序对象的创建须在 __init__.py 文件里完成
│  │  wsgi.py //wsgi服务启动
│  │
│  ├─controller //MVC中的控制器(C)
│  │     event_controller.py //事件控制器
│  │     api_controller.py //api控制器
│  │     user_controller.py //用户控制器
│  │     reminder_controller.py //提醒控制器
│  │  
│  ├─model //MVC中的模型,数据库中的表(M)
│  │     Event.py //事件表
│  │     User.py //用户表
│  │     EmailHistory //邮件历史记录
│  │
│  ├─views //MVC中的视图(V)
│  │     admin_view.py //管理员
│  │     event_view.py //事件
│  │     index_view.py //主页
│  │     user_view.py //用户
│  │     album_view.py //相册
│  │     reminder_view.py//提醒
│  │ 
│  ├─static //静态资源,css,js等
│  ├─templates //视图映射的页面
│  │      index.html //主页
│  │      create_event.html //创建事件
│  │      edit_event.html //编辑事件
│  │      event_list.html //查看事件
│  │      login.html //登录
│  │      register.html //注册
│  │      calendar.html //日历
│  │      profile.html //个人信息
│  │      album.html //相册
│  │      email_history.html //邮件页面
│  │      email_template.html //邮件模板
│  │      about.html //使用教程页面

模型-数据库的构建

项目开始项目构建数据库,也是MVC中的模型。数据库的搭建使用了flask-SQLAlchemy的orm框架,使用flask-migrate进行数据库的迁移。

User.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from app import db
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin


class User(db.Model, UserMixin):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    email = db.Column(db.String(255), nullable=False, unique=True)
    phone = db.Column(db.String(255), nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)  # 添加密码哈希字段
    is_admin = db.Column(db.Boolean, default=False, nullable=False)  # 新增 is_admin 字段

    event = db.relationship('Event', backref='user', lazy='dynamic')
    email_history = db.relationship('EmailHistory', backref='user', lazy='dynamic')

    def __init__(self, name, email, phone, password, is_admin=False):
        self.name = name
        self.email = email
        self.phone = phone
        self.set_password(password)  # 在初始化时设置密码哈希值
        self.is_admin = is_admin  # 设置用户是否是管理员

    def __repr__(self):
        return '<User %r>' % self.name

    # 添加密码哈希相关的方法
    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)

    # 实现 is_active 方法
    def is_active(self):
        return True  # 返回 True 表示用户是活动的,可以登录

Event.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from app import db
from app.model.User import User
from sqlalchemy.ext.hybrid import hybrid_property

class Event(db.Model):
    __tablename__ = 'events'
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey(User.id, ondelete='CASCADE'), nullable=False)
    title = db.Column(db.String(255), nullable=False)
    start_date = db.Column(db.Date, nullable=False)
    event_time = db.Column(db.Time)
    end_date = db.Column(db.Date)
    location = db.Column(db.String(255))
    description = db.Column(db.String(255))
    repeat = db.Column(db.String(255), nullable=False, default="none")  # Text field for custom repeat types
    reminder_type = db.Column(db.String(255), nullable=False, default="none")
    reminder_time = db.Column(db.Float, nullable=True, default=0)

    @hybrid_property
    def duration(self):
        return self.end_date - self.start_date

    def __init__(self, user_id, title, start_date, event_time, end_date, location, description, reminder_time,
                 repeat, reminder_type):
        self.user_id = user_id
        self.title = title
        self.start_date = start_date
        self.event_time = event_time
        self.end_date = end_date
        self.location = location
        self.description = description
        self.repeat = repeat
        self.reminder_type = reminder_type
        self.reminder_time = reminder_time

    def __repr__(self):
        return '<Event %r>' % self.title

EmailHistory.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# encoding:utf-8  
# encoding:utf-8
from datetime import datetime
from app import db
from app.model.User import User


class EmailHistory(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey(User.id, ondelete='CASCADE'), nullable=False)
    title = db.Column(db.String(255), nullable=False)
    content = db.Column(db.Text, nullable=False)
    recipients = db.Column(db.String(255), nullable=False)
    sent_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)

    def __init__(self, user_id, title, content, recipients):
        self.user_id = user_id
        self.title = title
        self.content = content
        self.recipients = recipients

创建好SQLAlchemy后使用flask-Migrate命令初始化数据库,使用数据库迁移可以很好的管理数据库结构的改变而且不会影响数据。

数据库迁移

创建迁移存储库:

1
flask db init

这会将迁移文件夹添加到应用程序中。此时,你可以发现项目目录多了一个 migrations 的文件夹,下边的 versions 目录下的文件就是生成的数据库迁移文件!

然后,运行以下命令生成迁移

1
flask db migrate -m "initial migration"

做完这两步就完成了第一次的初始迁移操作,我们可以看数据库已经有了我们创建的模型字段!之后,每次在新增和修改完模型数据之后,只需要执行以下两个命令即可

1
2
flask db migrate -m "description of changes"
flask db upgrade

这些步骤将允许你使用flask_migrate管理数据库模型的变化。请确保按照这些步骤在你的Flask应用中设置并使用flask_migrate,以便维护数据库模型的一致性。

使用Navicate查看数据模型:

image.png

以登录为例创建Web App

控制器

用户管理使用了flask-login管理用户逻辑 我们首先编写注册和登录逻辑,创建app/controller/user_controller.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
# encoding:utf-8
from flask import flash, redirect, url_for, render_template, make_response
from flask_login import login_user, login_required, logout_user, current_user
from app.forms import RegisterForm, LoginForm
from app.model.User import User
from app import db, login_manager


def register():
    """
    注册新用户。

    该函数处理新用户的注册过程。它验证用户提交的表单数据,
    检查用户是否已存在,在数据库中创建新用户,并将用户重定向到登录页面
    注册成功后。

    Returns:
        如果表单数据有效并且用户注册成功,则将用户重定向到登录页面。
        否则,它会呈现带有注册表单的 register.html 模板。

    """
    form = RegisterForm()
    if form.validate_on_submit():
        # 获取用户提交的数据
        name = form.name.data
        email = form.email.data
        phone = form.phone.data
        password = form.password.data

        # 检查用户是否已存在
        existing_user = User.query.filter_by(email=email).first()
        if existing_user:
            flash('该邮箱地址已被注册', 'error')
            return redirect(url_for('user.register'))

        # 创建新用户
        new_user = User(name=name, email=email, phone=phone, password=password)
        db.session.add(new_user)
        db.session.commit()

        flash('注册成功,请登录', 'success')
        return redirect(url_for('user.login'))
    return render_template('register.html', form=form)


def login():
    """
    用户登录函数

    如果用户已登录,则重定向到受保护的页面。
    如果用户提交了有效的登录表单,则验证用户凭据并将用户标记为已登录。
    如果用户验证失败,则显示错误消息。

    return: 渲染登录页面或重定向到受保护的页面
    """
    if current_user.is_authenticated:
        # 如果用户已登录,重定向到受保护的页面
        return redirect(url_for('event.show_event'))
    form = LoginForm()
    if form.validate_on_submit():
        email = form.email.data  # 修改为使用邮箱作为登录凭据
        password = form.password.data
        user = User.query.filter_by(email=email).first()
        if user and user.check_password(password):
            # 用户验证成功,将用户标记为已登录
            # 可以使用 Flask-Login 或自己的会话管理逻辑来处理登录状态
            login_user(user, remember=form.remember)
            # return redirect(url_for('event.show_event'))  # 跳转到用户仪表板或其他受保护的页面
            # 设置cookie
            response = make_response(redirect(url_for('event.show_event')))
            response.set_cookie('user_id', str(user.id))  # 存储用户ID在cookie中
            flash('登录成功', 'success')

            return response
        else:
            flash('登录失败,请检查邮箱或密码', 'error')

    return render_template('login.html', form=form)


@login_required
def profile():
    """
    用户个人信息页面
    """
    name = current_user.name
    email = current_user.email
    phone = current_user.phone

    return render_template('profile.html', name=name, email=email, phone=phone)


@login_required
def logout():
    logout_user()  # 使用 Flask-Login 注销用户
    flash('成功注销', 'success')
    return redirect(url_for('user.login'))


@login_manager.user_loader
def load_user(user_id):
    # 使用用户 ID 查询用户对象
    return User.query.get(int(user_id))

创建表单验证:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# forms.py  
from flask_wtf import FlaskForm  
from wtforms import StringField, PasswordField, validators, SubmitField, ValidationError, DateField, TimeField  
  
  
class RegisterForm(FlaskForm):  
	name = StringField('用户名', [validators.DataRequired("用户名不能为空")])  
	email = StringField('邮箱', [validators.DataRequired("邮箱不能为空"), validators.Email("无效的邮箱地址")])  
	phone = StringField('电话')  
	password = PasswordField('密码', [  
	validators.DataRequired("密码不能为空"),  
	validators.Length(min=8, message="密码长度至少 8 个字符")  
	])  
	confirm_password = PasswordField('确认密码', [  
	validators.DataRequired("请再次输入密码"),  
	validators.EqualTo('password', message='两次密码不匹配')  
	])  
  
  
class LoginForm(FlaskForm):  
	email = StringField('邮箱', [validators.DataRequired("邮箱不能为空"), validators.Email("无效的邮箱地址")])  
	password = PasswordField('密码', [validators.DataRequired("密码不能为空")])  
	remember = StringField('记住我')

视图

项目使用flask Blueprint(蓝图)来管理路由,创建app/views/user_view.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# user_view.py  
from flask import Blueprint  
from app.controller import user_controller  
  
user_blueprint = Blueprint('user', __name__)  
  
user_blueprint.route('/register', methods=['GET', 'POST'])(user_controller.register)  
user_blueprint.route('/login', methods=['GET', 'POST'])(user_controller.login)  
user_blueprint.route('/logout', methods=['GET'])(user_controller.logout)  
user_blueprint.route('/profile', methods=['GET', 'POST'])(user_controller.profile)

将参数通过jinja2传递到前端

注册页面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!DOCTYPE html>  
<html lang="en">  
<head>  
<meta charset="utf-8">  
<title>注册</title>  
<link rel="stylesheet" type="text/css" href="../static/css/register-login.css">  
<link rel="icon" href="../static/images/robot.png" type="image/x-icon"/>  
</head>  
<body>  
<div id="box"></div>  
<div class="cent-box register-box">  
<div class="cent-box-header">  
<h1 class="main-title hide">Pepper机器人</h1>  
<h2 class="sub-title">日程管理系统</h2>  
</div>  
  
<div class="cont-main clearfix">  
<div class="index-tab">  
<div class="index-slide-nav">  
<a href="{{ url_for('user.login') }}">登录</a>  
<a href="{{ url_for('user.register') }}" class="active">注册</a>  
<div class="slide-bar slide-bar1"></div>  
</div>  
</div>  
<form method="POST">  
{{ form.hidden_tag() }}  
<div class="login form">  
<div class="group">  
<div class="group-ipt name">  
<label for="name"></label>  
{% if form.name.data is not none %}  
<input type="text" name="name" id="name" class="ipt" value="{{ form.name.data }}" placeholder="输入您的用户名" required>  
{% else %}  
<input type="text" name="name" id="name" class="ipt" placeholder="输入您的用户名" required>  
{% endif %}  
</div>  
<div class="group-ipt email">  
<label for="email"></label>  
{% if form.email.data is not none %}  
<input type="email" name="email" id="email" class="ipt" value="{{ form.email.data }}" placeholder="请输入您的邮箱" required>  
{% else %}  
<input type="email" name="email" id="email" class="ipt" placeholder="请输入您的邮箱" required>  
{% endif %}  
</div>  
<div class="group-ipt phone">  
<label for="phone"></label>  
{% if form.phone.data is not none %}  
<input type="text" name="phone" id="phone" class="ipt" value="{{ form.phone.data }}" placeholder="输入您的电话" required>  
{% else %}  
<input type="text" name="phone" id="phone" class="ipt" placeholder="输入您的电话" required>  
{% endif %}  
</div>  
<div class="group-ipt password">  
<label for="password"></label><input type="password" name="password" id="password" class="ipt" placeholder="输入密码" required>  
</div>  
<div class="group-ipt confirm_password">  
<label for="confirm_password"></label><input type="password" name="confirm_password" id="confirm_password" class="ipt" placeholder="确认密码" required>  
</div>  
  
</div>  
<div class="button">  
<button type="submit" class="register-btn" id="button" value="注册">注册</button>  
</div>  
</div>  
</form>  
  
</div>  
</div>  
  
<script src='../static/js/particles.js' type="text/javascript"></script>  
<script src='../static/js/background.js' type="text/javascript"></script>  
<script src='../static/js/jquery.min.js' type="text/javascript"></script>  
<script src='../static/js/layer/layer.js' type="text/javascript"></script>  
<script src='../static/js/index.js' type="text/javascript"></script>  
</body>  
</html>

效果图: image.png

登录页面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!DOCTYPE html>  
<html lang="en">  
<head>  
<meta charset="utf-8">  
<title>登录</title>  
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/register-login.css') }}">  
<link rel="icon" href="{{ url_for('static', filename='images/robot.png') }}" type="image/x-icon"/>  
</head>  
<body>  
<div id="box"></div>  
<div class="cent-box">  
<div class="cent-box-header">  
<h1 class="main-title hide">Pepper机器人</h1>  
<h2 class="sub-title">日程管理系统</h2>  
</div>  
  
<div class="cont-main clearfix">  
<div class="index-tab">  
<div class="index-slide-nav">  
<a href="{{ url_for('user.login') }}" class="active">登录</a>  
<a href="{{ url_for('user.register') }}">注册</a>  
<div class="slide-bar"></div>  
</div>  
</div>  
<form method="POST">  
{{ form.hidden_tag() }}  
<div class="login form">  
<div class="group">  
<div class="group-ipt email">  
<label for="email"></label>  
{% if form.email.data is not none %}  
<input type="email" name="email" id="email" class="ipt" value="{{ form.email.data }}" placeholder="输入您的邮箱" required>  
{% else %}  
<input type="email" name="email" id="email" class="ipt" placeholder="输入您的邮箱" required>  
{% endif %}  
</div>  
<div class="group-ipt password">  
<label for="password"></label><input type="password" name="password" id="password" class="ipt" placeholder="输入您的登录密码" required>  
</div>  
</div>  
<div class="button">  
<button type="submit" class="login-btn register-btn" id="button" value="登录">登录</button>  
</div>  
<div class="remember clearfix">  
<label class="remember-me"><input type="checkbox" name="remember"> 记住我</label>  
</div>  
</div>  
</form>  
</div>  
</div>  
  
<script src='../static/js/particles.js' type="text/javascript"></script>  
<script src='../static/js/background.js' type="text/javascript"></script>  
<script src='../static/js/jquery.min.js' type="text/javascript"></script>  
<script src='../static/js/layer/layer.js' type="text/javascript"></script>  
<script src='../static/js/index.js' type="text/javascript"></script>  
</body>  
</html>

效果图:

image.png

配置config.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 调试模式是否开启  
DEBUG = True  
# 是否追踪对象的修改。  
SQLALCHEMY_TRACK_MODIFICATIONS = True  
# 查询时显示原始SQL语句  
SQLALCHEMY_ECHO = False  
# session必须要设置key  
SECRET_KEY = 'c798ee1f5fd894f6e0ba9fc0d16b8b22'  
# mysql数据库连接信息  
DATABASE = 'schedule'  
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://[user]:[password]@localhost/" + DATABASE

使用工厂函数初始化

app/_init_.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from flask import Flask  
from flask_sqlalchemy import SQLAlchemy  
from flask_login import LoginManager  
from flask_migrate import Migrate  
from flask_restful import Api  
from flask_admin import Admin  
from flask_mail import Mail  
from flask_uploads import configure_uploads  
import logging  
from logging.handlers import RotatingFileHandler  
from flask_apscheduler import APScheduler  
  
  
# 创建Flask应用实例  
db = SQLAlchemy() # 数据库实例  
login_manager = LoginManager() # 登录管理实例  
migrate = Migrate() # 数据库迁移实例   
  
  
def create_app() -> Flask:  
"""创建Flask应用实例并进行初始化设置。  
  
Returns:  
Flask: 初始化设置后的Flask应用实例。  
"""  
	app = Flask(__name__)  
	app.config.from_object('app.config')  
	app.config.from_envvar('FLASKR_CONFIGS')  
	  
	configure_logging(app) # 添加日志配置  
	initialize_extensions(app) # 初始化  
	register_blueprints(app) # 注册蓝图  
	
	return app  
  
def initialize_extensions(app: Flask) -> None:  
"""初始化扩展,包括数据库、登录管理、数据库迁移、邮件和文件上传。  
  
Args:  
app (Flask): Flask应用实例。  
"""  
	db.init_app(app)  
	migrate.init_app(app, db)  
	login_manager.init_app(app)  
	login_manager.login_view = 'user.login'  
  
  
def register_blueprints(app: Flask) -> None:  
"""注册蓝图,包括主页、用户、事件和提醒蓝图。  
  
Args:  
app (Flask): Flask应用实例。  
"""   
	from app.views.user_view import user_blueprint  
	
	app.register_blueprint(user_blueprint, url_prefix='/user')  


def configure_logging(app: Flask) -> None:  
"""配置应用程序日志。  
  
Args:  
app (Flask): Flask应用实例。  
"""  
	log_file_path = 'app.log'  
	log_handler = RotatingFileHandler(log_file_path, maxBytes=10240, backupCount=10)  
	log_handler.setFormatter(logging.Formatter(  
	'%(asctime)s %(levelname)s: %(message)s '  
	'[in %(pathname)s:%(lineno)d]'  
	))  
	log_handler.setLevel(logging.INFO)  
	app.logger.addHandler(log_handler)  
  

运行项目

app/runserver.py

1
2
3
4
5
from app import create_app  
  
if __name__ == '__main__':  
	app = create_app()  
	app.run(host='0.0.0.0', port=5000, debug=True)
0%