<repo_name>alone-zd/flask-blog<file_sep>/README.md
# flask-blog
**演示地址： http://alonezd.top**

博客整体采用flask框架，mysql数据库，进行上线开发。教程会在后面陆续更新
目前已经支持的功能：
1. 个人账户的注册、激活与登录
2. markdown的博文编辑功能及图片上传功能
3. 博文详情页页面的html解析及代码高亮等
4. 文章分页以及分类与分类展示
5. 全局搜索功能

使用：

#### １、建立mysql数据库，我的库名为(alone_blog)，或者改为你们自己的库，并在项目目录下的App/config.py里面进行更改。

#### 2、删除migrations文件，然后依次执行

- python3 manage.py db init
- python3 manage.py db migrate
- python3 manage.py db upgrade

#### 3、执行命令行进行启动

```python
python3 manage.py runserver
```

这个时候打开你的本地浏览器，输入 http://127.0.0.1:5000 就可以看到博客页面啦
<file_sep>/App/views/__init__.py
from .main import main  # 导入main蓝本对象
from .user import user  # 导入user蓝本对象
from .posts import posts  # 导入posts博客处理的蓝本对象
from .owncenter import owncenter  # 导入owncenter个人中心蓝本对象


# 循环迭代的蓝本配置
blueprint_config = [
    (main, ''),
    (user, ''),
    (posts, ''),
    (owncenter, ''),
]


# 注册蓝本对象的函数
def register_blueprint(app):
    # 循环迭代注册蓝本
    for blueprint,perfix in blueprint_config:
        app.register_blueprint(blueprint,url_prefix=perfix)<file_sep>/App/forms/__init__.py
from .user import Register,Login,AgainActive
from .posts import SendPosts,Comment,Catgs
from .owncenter import UserInfo,Upload<file_sep>/App/views/posts.py
from flask import Blueprint, render_template, flash, redirect, url_for, current_app
from sqlalchemy import or_

from App.forms import SendPosts, Comment, Catgs  # 导入发表博客与发表评论的表单类
from App.models import Posts,Categorys,User # 导入博客模型类和标签模型类
from flask_login import current_user, login_required

from datetime import datetime
from flask import jsonify, request
from App.extensions import qiniu_store, csrf
import os, base64
from App.extensions import file
from PIL import Image

posts = Blueprint('posts', __name__)


# 使用uuid扩展库生成唯一的名称
def random_filename(suffix):
    import uuid
    u = uuid.uuid4()
    return str(u) + '.' + suffix


# 图片缩放处理
def img_zoom(path, width=1000, height=250):
    # 打开文件
    img = Image.open(path)
    # 重新设计尺寸
    # img.thumbnail((width, height))
    # 保存缩放后的图片 保留原图片
    # 保存缩放
    img.save(path)


@posts.route('/send_posts/', methods=['GET', 'POST'])
def send_posts():
    form = SendPosts()
    ctgs = Categorys.query.all()
    # 判断登录后在进行发表
    if not current_user.is_authenticated:
        flash('您还没有登录  请登录后再发表')
    if current_user.id==1:
        if form.validate_on_submit():
            # md格式
            article = request.form['article']
            ctgs = request.form['ctgs']
            tags = Categorys.query.filter_by(categorys=ctgs).first()
            # 图片上传
            img = request.files.get('img')  # 获取上传对象
            ex = os.path.splitext(img.filename)[1]
            filename = datetime.now().strftime('%Y%m%d%H%M%S') + ex
            file = img.stream.read()
            qiniu_store.save(file, filename)
            post = Posts(title=form.title.data, user=current_user, article=article, img=qiniu_store.url(filename),tags=[tags])
            post.save()
            flash('博客发表成功')
            return redirect(url_for('main.index'))
    else:
        flash('您没有发表博客的权限！')
        return redirect(url_for('main.index'))
    return render_template('posts/send_posts.html', form=form, ctgs=ctgs)


# 增加分类
@posts.route('/add_catgs/', methods=['GET', 'POST'])
def add_catgs():
    form = Catgs()
    ctgs = Categorys.query.all()
    if not current_user.is_authenticated:
        flash('您还没有登录  请登录后再操作')
    elif form.validate_on_submit():
        catgs = Categorys(categorys=form.categorys.data)
        catgs.save()
        print(form.categorys.data)
        flash('分类添加成功')
        return redirect(url_for('posts.send_posts'))
    return render_template('posts/add_categorys.html', form=form, ctgs=ctgs)


# 删除分类
@posts.route('/delete_catgs/', methods=['GET', 'POST'])
def delete_catgs():
    ctgs = Categorys.query.all()
    form = Catgs()
    if not current_user.is_authenticated:
        flash('您还没有登录  请登录后再操作')
    elif form.validate_on_submit():
        cs = request.form['ctgs']
        d = Categorys.query.filter_by(categorys=cs)[0]
        d.delete()
        flash('分类删除成功')
        return redirect(url_for('posts.send_posts'))
    return render_template('posts/delete_categorys.html', form=form, ctgs=ctgs)


# 搜索
@posts.route('/search/', methods=['GET', 'POST'])
@csrf.exempt
def search():
    try:
        page = int(request.args.get('page'))
    except:
        page = 1
    # 获取搜索的关键字
    if request.method == 'POST':
        keyword = request.form.get('keyword', '')
    else:
        keyword = request.args.get('keyword', '')
    # 查询pid为0（是博客内容，不为0则是评论或回复内容），statue为0（所有人可见），按照时间降序显示
    # 搜索功能，搜索内容为：标题或正文
    pagination = Posts.query.filter(or_(Posts.title.contains(keyword), Posts.article.contains(keyword)), Posts.pid == 0,
                                    Posts.state == 0) \
        .order_by(Posts.timestamp.desc()) \
        .paginate(page, current_app.config['PAGE_NUM'], False)  # False 为不抛出异常
    data = pagination.items  # 获取page页面的数据
    # 将文章(pid=0)对应的的id查询出来，
    art = Posts.query.filter(Posts.pid==0).count()
    # 按照访问量排行降序查询，拿出前五
    visit = Posts.query.filter(Posts.pid == 0, Posts.state == 0).order_by(-Posts.visit)[:5]
    # 拿到文章对象，进行遍历，然后将访问量放进数组，进行求和
    article_list = Posts.query.filter(Posts.pid==0)
    list = []
    for i in article_list:
        list.append(i.visit)
    v=sum(list)

    cs = Categorys.query.all()
    comments_five = Posts.query.filter(Posts.pid!=0)[0:5]
    return render_template('posts/search_detail.html', pagination=pagination, posts=data, keyword=keyword,art = art, visit=visit, v=v, ctgs=cs,comments_five=comments_five)


# 博客分类
@posts.route('posts_ctgs',methods=['GET','POST'])
def posts_ctgs():
    try:
        page = int(request.args.get('page'))
    except:
        page = 1
    cid = request.args.get('pid')
    global pagination,data
    posts = Categorys.query.get(cid)
    for i in posts.posts:
        pagination = Posts.query.filter(Posts.title==i.title).order_by(Posts.timestamp.desc()).paginate(page, current_app.config['PAGE_NUM'], False)  # False 为不抛出异常
        data = pagination.items  # 获取page页面的数据
    category = Categorys.query.get(cid)
    # 将文章(pid=0)对应的的id查询出来，
    art = Posts.query.filter(Posts.pid==0).count()
    # 按照访问量排行降序查询，拿出前五
    visit = Posts.query.filter(Posts.pid == 0, Posts.state == 0).order_by(-Posts.visit)[:5]
    # 拿到文章对象，进行遍历，然后将访问量放进数组，进行求和
    article_list = Posts.query.filter(Posts.pid==0)
    list = []
    for i in article_list:
        list.append(i.visit)
    v=sum(list)

    cs = Categorys.query.all()
    comments_five = Posts.query.filter(Posts.pid!=0)[0:5]

    return render_template('posts/posts_ctgs.html',pagination=pagination,posts=data,category=category,art = art, visit=visit, v=v, ctgs=cs,comments_five=comments_five)


# 博客详情
@posts.route('/posts_detail/<int:pid>/')
def posts_detail(pid):
    # 查询当前博客的内容 pid为博客的自增id 并不是博客模型字段的pid
    p = Posts.query.get(pid)
    p.visit += 1  # 博客的访问量
    p.save()
    form = Comment()  # 发表评论和回复的表单类
    # 查询出博客的评论内容和回复，按照博客内容和回复在一起的排序顺序，
    # 过滤条件为博客path中包含博客id的，这样博客就将博客的内容过滤出去，只查询评论和留言
    comment = Posts.query.filter(Posts.path.contains(str(pid))).order_by(Posts.path.concat(Posts.id))
    ctgs = Categorys.query.all()
    comment_nums = Posts.query.filter(Posts.path.contains(str(pid))).count()

    return render_template('posts/posts_detail.html', posts=p, form=form, comment=comment, ctgs=ctgs, comment_nums=comment_nums)


# 评论和回复
@posts.route('/comment/', methods=['GET', 'POST'])
@login_required
def comment():
    pid = request.form.get('pid')
    rid = request.form.get('rid')
    # 判断当前是评论还是回复，如果为评论则为假，否则为真
    if rid:
        id = rid
    else:
        id = pid
    p = Posts.query.get(int(id))
    Posts(article=request.form.get('article'), pid=id, path=p.path + id + ',', user=current_user).save()

    return redirect(url_for('posts.posts_detail', pid=pid))


# editor.md的图片上传
@posts.route('/upload_image/', methods=['POST'])
@login_required
@csrf.exempt
def upload_image():
    data = request.files.get('editormd-image-file')
    if not data:
        res = {
            'success': 0,
            'message': '图片失败请重试'
        }
    else:
        ex = os.path.splitext(data.filename)[1]
        filename = datetime.now().strftime('%Y%m%d%H%M%S') + ex
        file = data.stream.read()
        qiniu_store.save(file, filename)
        res = {
            'success': 1,
            'message': '图片上传成功',
            'url': qiniu_store.url(filename)
        }
    return jsonify(res)
<file_sep>/App/__init__.py
from flask import Flask
from App.views import register_blueprint  # 导入views注册蓝本对象的方法
from .config import configDict  # 导入配置类的字典
from .extensions import init_app  # 导入初始化扩展库的init_app方法
from App.models import Posts


# 加载整个App项目并返回App对象
def create_app(configName='default'):
    app = Flask(__name__)
    # 加载配置类
    app.config.from_object(configDict[configName])
    from datetime import timedelta
    app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(seconds=1)
    # 注册蓝本
    register_blueprint(app)
    # 初始化扩展库
    init_app(app)
    # 添加自定义模板过滤器
    addTemFilter(app)

    return app


# 自定义模板过滤器
def addTemFilter(app):
    # 内容超出给定的长度  显示...
    def showEllipsis(Str, length=150):
        if len(Str)>length:
            Str = Str[0:length]+' ·  ·  ·  ·  ·  ·  ·  ·  '
        return Str

    # 获取回复人的名称的过滤器
    def replayName(pid):
        username = Posts.query.get(int(pid)).user.username
        return username
    app.add_template_filter(showEllipsis)
    app.add_template_filter(replayName)
<file_sep>/App/models/user.py
from App.extensions import db  # 导入模型对象
from .db_base import DB_Base  # 导入自定义模型基类
from datetime import datetime
# 导入生成与验证密码的方法
from werkzeug.security import generate_password_hash, check_password_hash
from itsdangerous import TimedJSONWebSignatureSerializer as Seralize  # 导入序列化方法
from flask import current_app
from flask_login import UserMixin  # 包含了判断是否登录等等方法饿基类
from App.extensions import login_manager


# User  模型类
class User(UserMixin,db.Model, DB_Base):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)  # 自增id
    username = db.Column(db.String(12), index=True, unique=True)  # 用户名
    password_hash = db.Column(db.String(128))  # 加密的密码
    sex = db.Column(db.Boolean, default=True)  # 性别
    age = db.Column(db.Integer, default=18)  # 年龄
    email = db.Column(db.String(50), unique=True)  # 邮箱
    icon = db.Column(db.String(70), default='default.jpg')  # 头像
    lastLogin = db.Column(db.DateTime)  # 上次登录时间
    registerTime = db.Column(db.DateTime, default=datetime.utcnow)  # 注册时间
    confirm = db.Column(db.Boolean, default=False)  # 激活状态，默认未激活

    # 设置模型引用关系
    """
    Posts : 建立引用关系的模型
    backref : 是给关联的模型添加一个查询的属性  叫user
    lazy : 加载的时机  返回查询集（可以添加连贯操作）
    """
    posts = db.relationship('Posts',backref='user',lazy='dynamic')

    # 密码加密处理的装饰器
    @property
    def password(self):
        raise AttributeError

    @password.setter
    def password(self, password):
        self.password_hash = <PASSWORD>_password_hash(password)

    # 检验密码正确性的方法
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

    # 生成token用于账户激活（也就是知道事哪个用户激活的）
    def generate_token(self):
        s = Seralize(current_app.config['SECRET_KEY'])
        return s.dumps({'id': self.id})  # 返回加密的字符串

    #  检测传递过来的token值， 进行账户激活
    @staticmethod
    def check_token(token):
        s = Seralize(current_app.config['SECRET_KEY'])
        try:
            id = s.loads(token)['id']
            u = User.query.get(id)
            if not u:
                return False
            u.confirm = True
            u.save()
            return True
        except:
            return False


# 这是一个回调的方法，实时获取当前表中用户数据的对象
@login_manager.user_loader
def user_loader(userid):
    return User.query.get(int(userid))


<file_sep>/App/models/posts.py
from App.extensions import db
from .db_base import DB_Base
from datetime import datetime

posts_categorys = db.Table('posts_categorys',
                       db.Column('posts_id', db.Integer, db.ForeignKey('posts.id'), primary_key=True),
                       db.Column('categorys_id', db.Integer, db.ForeignKey('categorys.id'), primary_key=True)
                       )
# 博客模型
class Posts(DB_Base, db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(20), index=True)  # 标题
    article = db.Column(db.Text)  # md格式正文
    pid = db.Column(db.Integer, default=0)  # 父id
    path = db.Column(db.Text, default='0,')  # 路径
    visit = db.Column(db.Integer, default=0)  # 文章访问量
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)  # 发表时间
    state = db.Column(db.Integer, default=0)  # 是否所有人可见
    img = db.Column(db.String(70))  # 图片

    tags = db.relationship('Categorys', secondary=posts_categorys, backref=db.backref('posts'))

    u_id = db.Column(db.Integer, db.ForeignKey('user.id'))  # 关联主表user的自增id



class Categorys(DB_Base, db.Model):
    __tablename__ = 'categorys'
    id = db.Column(db.Integer, primary_key=True)
    categorys = db.Column(db.String(20),index=True, unique=True)









<file_sep>/migrations/versions/f9c3ac552084_.py
"""empty message

Revision ID: f9c3ac552084
Revises: <PASSWORD>
Create Date: 2020-04-08 16:54:02.195068

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision = 'f9c3ac552084'
down_revision = '<PASSWORD>'
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_constraint('posts_ibfk_2', 'posts', type_='foreignkey')
    op.drop_column('posts', 'c_id')
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('posts', sa.Column('c_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True))
    op.create_foreign_key('posts_ibfk_2', 'posts', 'categorys', ['c_id'], ['id'])
    # ### end Alembic commands ###
<file_sep>/App/forms/user.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Length, EqualTo, Email, ValidationError
from App.models import User  # 导入User模型类


# 注册表单类
class Register(FlaskForm):
    username = StringField('用户名', validators=[DataRequired('用户名不能为空'), Length(min=4, max=12, message='用户名在4-12位之间')],
                           render_kw={'placeholder': '请输入用户名...'})
    userpass = PasswordField('密码', validators=[DataRequired('密码不能为空')], render_kw={'placeholder': '请输入密码...'})
    confirm = PasswordField('确认密码', validators=[DataRequired('确认密码不能为空...'), EqualTo('userpass', message='密码和确认密码不一致')],
                            render_kw={'placeholder': '请输入确认密码...'})
    email = StringField('激活邮箱', validators=[DataRequired('邮箱地址不能为空'), Email(message='请输入正确的邮箱地址')],
                        render_kw={'placeholder': '请输入用于账户激活的邮箱地址...'})
    submit = SubmitField('注册')

    # 验证用户名是否存在
    def validate_username(self, field):
        if User.query.filter_by(username=field.data).first():
            raise ValidationError('该用户名已存在，请重新输入')

    # 验证邮箱是否存在
    def validate_email(self, field):
        if User.query.filter_by(email=field.data).first():
            raise ValidationError('该邮箱已存在，请重新输入')


# 登录表单类
class Login(FlaskForm):
    username = StringField('用户名', validators=[DataRequired('用户名不能为空'), Length(min=4, max=12, message='用户名在4-12位之间')],
                           render_kw={'placeholder': '请输入用户名...'})
    userpass = PasswordField('密码', validators=[DataRequired('密码不能为空')], render_kw={'placeholder': '请输入密码...'})
    remember = BooleanField('记住我', default='checked')
    submit = SubmitField('登录')

    # 验证用户名是否存在
    def validate_username(self, field):
        if not User.query.filter_by(username=field.data).first():
            raise ValidationError('请输入正确的用户名或密码')


# 再次激活表单类
class AgainActive(FlaskForm):
    username = StringField('用户名', validators=[DataRequired('用户名不能为空'), Length(min=4, max=12, message='用户名在4-12位之间')],
                           render_kw={'placeholder': '请输入用户名...'})
    userpass = PasswordField('密码', validators=[DataRequired('密码不能为空')], render_kw={'placeholder': '请输入密码...'})
    submit = SubmitField('激活')

    # 验证用户名是否存在
    def validate_username(self, field):
        if not User.query.filter_by(username=field.data).first():
            raise ValidationError('请输入正确的用户名或密码')<file_sep>/requirements.txt
alembic==1.3.3
bleach==3.1.2
blinker==1.4
certifi==2019.11.28
chardet==3.0.4
Click==7.0
dominate==2.4.0
Flask==1.1.1
Flask-Bootstrap==3.3.7.1
Flask-Login==0.4.1
Flask-Mail==0.9.1
Flask-Markdown==0.3
flask-markdown2==0.0.3
Flask-Migrate==2.5.2
Flask-Moment==0.9.0
Flask-QiniuStorage==0.9.5
Flask-Script==2.0.6
Flask-SQLAlchemy==2.4.1
Flask-Uploads==0.2.1
Flask-WTF==0.14.2
idna==2.9
itsdangerous==1.1.0
Jinja2==2.11.1
jinja2-markdown==0.0.3
Mako==1.1.1
Markdown==3.2.1
MarkupSafe==1.1.1
mysql-connector==2.2.9
Pillow==7.0.0
PyMySQL==0.9.3
python-dateutil==2.8.1
python-editor==1.0.4
qiniu==7.2.6
requests==2.23.0
six==1.14.0
SQLAlchemy==1.3.13
urllib3==1.25.8
visitor==0.1.3
webencodings==0.5.1
Werkzeug==0.16.1
WTForms==2.2.1
<file_sep>/migrations/versions/0403e1b46505_.py
"""empty message

Revision ID: 0403e1b46505
Revises: <PASSWORD>
Create Date: 2020-04-07 22:32:53.535757

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision = '<KEY>'
down_revision = '<PASSWORD>'
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('posts_categorys',
    sa.Column('posts_id', sa.Integer(), nullable=False),
    sa.Column('categorys_id', sa.Integer(), nullable=False),
    sa.ForeignKeyConstraint(['categorys_id'], ['categorys.id'], ),
    sa.ForeignKeyConstraint(['posts_id'], ['posts.id'], ),
    sa.PrimaryKeyConstraint('posts_id', 'categorys_id')
    )
    op.drop_constraint('categorys_ibfk_1', 'categorys', type_='foreignkey')
    op.drop_column('categorys', 'aid')
    op.add_column('posts', sa.Column('u_id', sa.Integer(), nullable=True))
    op.drop_constraint('posts_ibfk_2', 'posts', type_='foreignkey')
    op.create_foreign_key(None, 'posts', 'user', ['u_id'], ['id'])
    op.drop_column('posts', 'uid')
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('posts', sa.Column('uid', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True))
    op.drop_constraint(None, 'posts', type_='foreignkey')
    op.create_foreign_key('posts_ibfk_2', 'posts', 'user', ['uid'], ['id'])
    op.drop_column('posts', 'u_id')
    op.add_column('categorys', sa.Column('aid', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True))
    op.create_foreign_key('categorys_ibfk_1', 'categorys', 'posts', ['aid'], ['id'])
    op.drop_table('posts_categorys')
    # ### end Alembic commands ###
<file_sep>/App/extensions.py
from flask_bootstrap import Bootstrap
from flask_sqlalchemy import SQLAlchemy  # ORM模型扩展库
from flask_migrate import Migrate # 模型迁移
from flask_mail import Mail  # 邮件发送扩展库
from flask_login import LoginManager  # 处理用户登录的扩展库
from flask_moment import Moment  # 格式化时间显示的扩展库
from flask_uploads import IMAGES, UploadSet,configure_uploads,patch_request_class  # 导入头像上传
from flask_qiniustorage import Qiniu  # 七牛云上传
from flask_wtf.csrf import CSRFProtect

# 实例化
bootstrap = Bootstrap()  # bootstrap扩展库
db = SQLAlchemy()  # ORM模型
migrate = Migrate()  # 模型迁移
mail = Mail()  # 邮件发送
login_manager = LoginManager()  # 处理登录的库
moment = Moment()  # 格式化时间显示的扩展库
file = UploadSet('photos',IMAGES)  # 上传头像的
qiniu_store = Qiniu()
csrf = CSRFProtect()


# 加载app
def init_app(app):
    bootstrap.init_app(app)
    db.init_app(app)
    migrate.init_app(app=app, db=db)
    mail.init_app(app)
    moment.init_app(app)
    login_manager.init_app(app)
    qiniu_store.init_app(app)
    csrf.init_app(app)

    login_manager.login_view = 'user.login'  # 当未登陆时，访问了需要登录的路由时，进行登录的端点
    login_manager.session_protection = 'basic'  # 设置session的保护级别

    # 配置文件上传（头像）
    configure_uploads(app,file)
    patch_request_class(app, size=None)




<file_sep>/App/forms/posts.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
from flask_wtf.file import FileAllowed, FileRequired, FileField
from App.extensions import file  # 导入文件上传配置对象


# 发表博客的表单类
class SendPosts(FlaskForm):
    title = StringField('标题', validators=[DataRequired('标题内容不能为空'),
                                          Length(min=3,max=100,message='标题内容在3-100字之间')],render_kw={'placeholder':'请输入标题'})
    article = TextAreaField('博客内容', validators=[DataRequired('博客内容不能为空')],render_kw={'placeholder':'请输入博客内容'})

    submit = SubmitField('发表博客')


# 发表评论和回复的表单类
class Comment(FlaskForm):
    article = TextAreaField('评论内容', validators=[DataRequired('评论内容不能为空')])
    submit = SubmitField('发表评论')


# 添加分类的表单
class Catgs(FlaskForm):
    categorys = StringField('分类专栏',
                            render_kw={'placeholder': '请输入分类'})
<file_sep>/manage.py
from flask import Flask
from flask_script import Manager
# 导入创建App，并加载整个项目的方法
from App import create_app
from flask_migrate import MigrateCommand


# 调用
app = create_app()
manager = Manager(app)
manager.add_command('db', MigrateCommand)


if __name__ == '__main__':
    manager.run()<file_sep>/App/views/user.py
from flask import Blueprint,render_template,flash,url_for,redirect
from App.forms import Register, Login, AgainActive  # 导入表单注册类
from App.models import User  # 导入User模型类
from App.email import send_mail  # 导入发送邮件的函数
from flask_login import login_required, logout_user, login_user, current_user
from datetime import datetime


user = Blueprint('user', __name__)


@user.route('/register/', methods=['GET', 'POST'])
def register():
    # 实例化注册表单类
    form = Register()
    if form.validate_on_submit():
        # 实例化存储注册表单数据
        u = User(username=form.username.data, password=<PASSWORD>,email=form.email.data)
        u.save()
        # 调用获取token的方法
        token = u.generate_token()
        # 发送邮件激活
        send_mail('账户激活', form.email.data,username=form.username.data, token=token)
        flash('注册成功，请前去邮箱激活')
        # 成功去登录
        return redirect(url_for('user.login'))
    return render_template('user/register.html', form=form)


# 进行账户激活的视图
@user.route('/active/<token>')
def active(token):
    if User.check_token(token):
        flash('激活成功，请前去登录...')
        # 激活成功跳转到登录
        return redirect(url_for('user.login'))
    else:
        flash('激活失败，请重新进行账户激活操作....')
        return redirect(url_for('user.again_active'))


# 再次激活的视图
@user.route('/again_active/',methods=['GET', 'POST'])
def again_active():
    form = AgainActive()
    if form.is_submitted():
        u = User.query.filter(User.username == form.username.data).first()
        if not u:
            flash('请输入正确的用户名或密码....')
        elif not u.check_password(form.userpass.data):
            flash('请输入正确的用户名或密码')
        elif not u.confirm:
            # 调用获取token的方法
            token = u.generate_token()
            # 发送邮件激活
            send_mail('账户再次激活', u.email,tem='again_active', username=form.username.data, token=token)
            flash('激活邮件发送成功，请前去邮箱激活')
        else:
            flash('该账户已经激活，请前去登录...')
    return render_template('user/again_active.html', form=form)


# 登录视图
@user.route('/login/', methods=['GET', 'POST'])
def login():
    form = Login()
    if form.validate_on_submit():
        u = User.query.filter(User.username == form.username.data).first()
        if not u:
            flash('请输入正确的用户名或密码')
        elif not u.confirm:
            flash('您还没有进行激活，请前去激活账户')
            return redirect(url_for('user.again_active'))
        elif not u.check_password(form.userpass.data):
            flash('请输入正确的用户名或密码')
        else:
            flash('登录成功')
            # 修改登录时间
            u.lastLogin = datetime.now()
            u.save()
            login_user(u, remember=form.remember.data)  # 使用第三方扩展库处理登录状态的维持
            return redirect(url_for('main.index'))

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


# 退出登录
@user.route('logout')
def logout():
    logout_user()
    flash('退出成功')
    return redirect(url_for('main.index'))<file_sep>/App/models/db_base.py
from App.extensions import db


# 自定义封装模型基类
class DB_Base:
    # 添加一条数据
    def save(self):
        try:
            # self 代表当前当前实例化的对象
            db.session.add(self)
            db.session.commit()
            return True
        except:
            db.session.rollback()
            return False

    # 添加多条数据
    @staticmethod
    def save_all(*args):
        try:
            db.session.add_all(args)
            db.session.commit()
            return True
        except:
            db.session.rollback()
            return False

    # 删除
    def delete(self):
        try:
            db.session.delete(self)
            db.session.commit()
            return True
        except:
            db.session.rollback()
            return False<file_sep>/App/views/main.py
from flask import Blueprint,render_template, request,current_app

from App.models import Posts,Categorys
from sqlalchemy import func

main = Blueprint('main', __name__)


# 首页视图
@main.route('/')
@main.route('/index/')
def index():
    try:
        page = int(request.args.get('page',1))
    except:
        page = 1
    # 查询pid为0（是博客内容，不为0则是评论或回复内容），statue为0（所有人可见），按照时间降序显示
    pagination = Posts.query.filter(Posts.pid==0, Posts.state==0).order_by(Posts.timestamp.desc())\
        .paginate(page, current_app.config['PAGE_NUM'], False)  # False 为不抛出异常
    data = pagination.items  # 获取page页面的数据
    # 将文章(pid=0)对应的的id查询出来，
    art = Posts.query.filter(Posts.pid==0).count()
    # 按照访问量排行降序查询，拿出前五
    visit = Posts.query.filter(Posts.pid == 0, Posts.state == 0).order_by(-Posts.visit)[:5]
    # 拿到文章对象，进行遍历，然后将访问量放进数组，进行求和
    article_list = Posts.query.filter(Posts.pid==0)
    list = []
    for i in article_list:
        list.append(i.visit)
    v=sum(list)

    cs = Categorys.query.all()
    comments_five = Posts.query.filter(Posts.pid!=0)[0:5]


    return render_template('main/index.html', posts=data, pagination=pagination, art = art, visit=visit, v=v, ctgs=cs,comments_five=comments_five)
<file_sep>/uwsgi.ini
[uwsgi]


socket = 0.0.0.0:8000

chdir =  /home/flask-blog/

wsgi-file = manage.py


callable =app
processes = 1

threads = 2
<file_sep>/App/config.py
import os
from datetime import timedelta

BASE_DIR = os.path.abspath(os.path.dirname(__file__))


# 配置类的基类
class Config:
    SECRET_KEY = 'alone'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    BOOTSTRAP_SERVER_LOCAL = True  # 加载本地静态资源文件
    # 邮件发送配置
    # 端口号
    MAIL_PORT = 465
    MAIL_USE_SSL = True
    # 邮箱服务器
    MAIL_SERVER = 'smtp.163.com'
    # 用户名
    MAIL_USERNAME = '<EMAIL>'
    # 密码
    MAIL_PASSWORD = '<PASSWORD>'
    # 分页每页显示数据条数
    PAGE_NUM = 6
    # 文件上传配置
    UPLOADED_PHOTOS_DEST = os.path.join(BASE_DIR, 'static/upload')
    MAX_CONTENT_LENGTH = 1024 * 1024 * 64
    # 七牛云上传配置
    QINIU_ACCESS_KEY = "<KEY>"
    QINIU_SECRET_KEY = "<KEY>"
    QINIU_BUCKET_NAME = "aloneblog"
    QINIU_BUCKET_DOMAIN = 'cdnonline.top'


# 开发环境
class DevelopmentConfig(Config):
    SQLALCHEMY_DATABASE_URI = 'mysql+mysqlconnector://root:128114@127.0.0.1:3306/alone_blog'
    DEBUG = True
    TESTING = False


# 测试环境
class TestingConfig(Config):
    SQLALCHEMY_DATABASE_URI = 'mysql+mysqlconnector://root:128114@127.0.0.1:3306/test_blog'
    DEBUG = False
    TESTING = True


# 生产环境
class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = 'mysql+mysqlconnector://root:128114@127.0.0.1:3306/blog'
    DEBUG = False
    TESTING = False


# 配置类的字典,给类起别名
configDict = {
    'default': DevelopmentConfig,
    'dev': DevelopmentConfig,
    'Test': TestingConfig,
    'production': ProductionConfig,
}
<file_sep>/App/forms/owncenter.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, IntegerField, RadioField
from wtforms.validators import DataRequired, NumberRange
from flask_wtf.file import FileAllowed, FileRequired, FileField
from App.extensions import file  # 导入文件上传配置对象


# 修改与查看个人信息的表单类
class UserInfo(FlaskForm):
    username = StringField('用户名', render_kw={'readonly': 'true'})
    sex = RadioField(label='性别', choices=[('1', '男'), ('0', '女')], validators=[DataRequired('性别必选')])
    age = IntegerField('年龄', validators=[DataRequired('年龄不能为空'), NumberRange(min=1, max=99, message='年龄在1~99之间')])
    email = StringField('邮箱', render_kw={'readonly': 'true'})
    lastLogin = StringField('上次登录时间', render_kw={'disabled': 'true'})
    register = StringField('注册时间', render_kw={'disabled': 'true'})
    submit = SubmitField('修改')


# 文件上传
class Upload(FlaskForm):
    icon = FileField('头像上传', validators=[FileRequired('您还没有选择上传的头像'),FileAllowed(file, message='该文件类型不允许上传')])
    submit = SubmitField('上传')<file_sep>/App/views/owncenter.py
from datetime import datetime

from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app

from App.models import Posts, Categorys
from App.forms import UserInfo  # 导入个人信息显示user模型类和文件上传表单类
from App.forms import SendPosts,Upload  # 用于编辑博客
from flask_login import current_user, login_required
from App.extensions import db, file, qiniu_store
import os
from App.extensions import file
from PIL import Image

owncenter = Blueprint('owncenter', __name__)


# 查看与修改个人信息
@owncenter.route('/user_info/', methods=['GET', 'POST'])
def user_info():
    form = UserInfo()
    if form.validate_on_submit():
        current_user.age = form.age.data

    form.username.data = current_user.username
    form.age.data = current_user.age
    form.sex.data = str(int(current_user.sex))
    form.email.data = current_user.email
    form.lastLogin.data = current_user.lastLogin
    form.register.data = current_user.registerTime
    return render_template('owncenter/user_info.html', form=form)


# 博客管理
@owncenter.route('/posts_manager/')
def posts_manager():
    try:
        page = int(request.args.get('page', 1))
    except:
        page = 1
    # 查询当前用户发表的所有博客，pid为0证明是博客内容，而不是评论和回复内容。state=0是所有人可见，
    # 按照时间进行降序查询
    posts = current_user.posts.filter_by(pid=0, state=0).order_by(Posts.timestamp.desc())
    # 获取page页面的数据
    pagination = Posts.query.filter(Posts.pid == 0, Posts.state == 0).order_by(Posts.timestamp.desc()) \
        .paginate(page, current_app.config['PAGE_NUM'], False)  # False 为不抛出异常
    data = pagination.items  # 获取page页面的数据

    return render_template('owncenter/posts_manager.html', posts=posts, pagination=pagination)


# 博客删除
@owncenter.route('/del_posts/<int:pid>')
@login_required
def del_posts(pid):
    # 查询博客
    p = Posts.query.get(pid)
    # 判断该博客是否存在
    if p:
        # 执行删除
        flash('删除成功')
        p.delete()  # 删除博客内容
        comment = Posts.query.filter(Posts.path.contains(str(pid)))  # 删除评论和留言的内容
        for posts in comment:
            posts.delete()
    else:
        flash('您要删除的博客不存在')
    return redirect(url_for('owncenter.posts_manager'))


# 使用uuid扩展库生成唯一的名称
def random_filename(suffix):
    import uuid
    u = uuid.uuid4()
    return str(u) + '.' + suffix


# 图片缩放处理
def img_zoom(path, width=100, height=100):
    # 打开文件
    img = Image.open(path)
    # 重新设计尺寸
    img.thumbnail((width, height))
    # 保存缩放后的图片 保留原图片
    # 保存缩放
    img.save(path)


#  头像上传
@owncenter.route('/upload/', methods=['GET', 'POST'])
@login_required
def upload():
    form = Upload()
    if form.validate_on_submit():
        icon = request.files.get('icon')  # 获取上传对象
        suffix = icon.filename.split('.')[-1]  # 获取后缀
        newName = random_filename(suffix)  # 获取新图片的名称
        # 以新名称保存图片
        file.save(icon, name=newName)
        delPath = current_app.config['UPLOADED_PHOTOS_DEST']
        # 删除之前上传的图片
        if current_user.icon != 'default.jpg':  # 如果不等于，则证明上传了头像
            os.remove(os.path.join(delPath, current_user.icon))
        current_user.icon = newName  # 更改当前对象的图片名称
        db.session.add(current_user)  # 更新到数据库中
        db.session.commit()
        # 拼接图片路径
        path = os.path.join(delPath, newName)
        # 进行缩放
        img_zoom(path)

    return render_template('owncenter/upload.html', form=form)


# 博客编辑
@owncenter.route('/edit_posts/<int:pid>', methods=['POST', 'GET'])
def edit_posts(pid):
    form = SendPosts()  # 实例化表单
    p = Posts.query.get(pid)  # 根据博客id  查询
    ctgs = Categorys.query.all()
    if not p:
        flash('该博客不存在')
        return redirect(url_for('owncenter.posts_manager'))
    if form.validate_on_submit():
        # md格式
        article = request.form['article']
        # 得到所选的分类值
        ctgs = request.form['ctgs']
        tags = Categorys.query.filter_by(categorys=ctgs).first()
        # 图片上传
        img = request.files.get('img')  # 获取上传对象
        ex = os.path.splitext(img.filename)[1]
        filename = datetime.now().strftime('%Y%m%d%H%M%S') + ex
        file = img.stream.read()
        qiniu_store.save(file, filename)

        p.title = form.title.data
        p.article = article
        p.img = qiniu_store.url(filename)
        p.tags = [tags]
        p.save()
        flash('博客更新成功')
        return redirect(url_for('owncenter.posts_manager'))
    form.title.data = p.title
    form.article.data = p.article

    return render_template('owncenter/edit_posts.html', form=form, ctgs=ctgs)
<file_sep>/App/models/__init__.py
from .user import User
from .posts import Posts,Categorys
<file_sep>/App/email.py
from flask import render_template, current_app
# 导入线程类
from threading import Thread
from flask_mail import Message
from App.extensions import mail


# 异步执行耗时操作
def async_send_mail(app, msg):
    with app.app_context():
        mail.send(message=msg)


def send_mail(subject, to, tem='active', **kwargs):
    app = current_app._get_current_object()
    # 创建邮件对象
    msg = Message(subject=subject, recipients=[to], sender=app.config['MAIL_USERNAME'])
    # 邮件主体内容
    msg.html = render_template('email/' + tem + '.html', **kwargs)
    # 发送邮件
    # 创建线程
    thr = Thread(target=async_send_mail, args=(app, msg))
    # 启动线程
    thr.start()
    return '发送邮件'<file_sep>/migrations/versions/1ed826de7a78_.py
"""empty message

Revision ID: 1<PASSWORD>
Revises: 685d91fb9b42
Create Date: 2020-04-07 21:03:54.087500

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision = '1<PASSWORD>'
down_revision = '6<PASSWORD>'
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('categorys', sa.Column('aid', sa.Integer(), nullable=True))
    op.create_foreign_key(None, 'categorys', 'posts', ['aid'], ['id'])
    op.drop_constraint('posts_ibfk_1', 'posts', type_='foreignkey')
    op.drop_column('posts', 'cid')
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('posts', sa.Column('cid', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True))
    op.create_foreign_key('posts_ibfk_1', 'posts', 'categorys', ['cid'], ['id'])
    op.drop_constraint(None, 'categorys', type_='foreignkey')
    op.drop_column('categorys', 'aid')
    # ### end Alembic commands ###
