Flask项目新经资讯项目总结


新经资讯是一个包含新闻首页,新闻详情,用户中心和后台管理等模块的Flask项目

  1. 项目框架的搭建

    • 包含配置信息:mysql,redis,session等
    • 工厂函数产出多种配置的app,数据库对象db,redis_store等
    • 日志的记录
    • 第三方库:云通讯,七牛云
    • 设置的常量,自定义状态码,commons,models等
    • flask_script,flask_migrate等
  2. 根据需求分析E-R图,构建模型类

    ![](D:\Users\Liu xiangyu\Desktop\新经资讯E-R图.png)

  3. 新闻首页和详情页模块

    3.1 登录/注册/登出/状态保持(略),以下代码生成图片验证码,在form表单打开时调用

    # 后端返回图片验证码使用响应体对象
    resp = make_response(image)
    # 设置内容类型
    resp.headers['Content-Type'] = 'image/jpg'
    return resp
    
    // 生成一个图片验证码的编号,并设置页面中图片验证码img标签的src属性
    function generateImageCode() {
      // 1. 生成一个编号
      // 严格一点的使用uuid保证编号唯一, 不是很严谨的情况下,也可以使用时间戳
      imageCodeId = generateUUID();
      // 2. 拼接验证码地址
      var imageCodeUrl = "/passport/image_code?code_id=" + imageCodeId;
      // 3. 设置页面中图片验证码img标签的src属性
      $(".get_pic_code").attr("src", imageCodeUrl)}
    
    // 一个最基础的ajax请求,负责给后端发送数据
    var params = {
        "mobile": mobile,
        "smscode": smscode,
        "password": password,}
      $.ajax({
        url:"/passport/register",
        type: "post",
        headers: {
        "X-CSRFToken": getCookie("csrf_token")},
        data: JSON.stringify(params),
        contentType: "application/json",
        success: function (resp) {
          if (resp.errno == "0"){
            // 刷新当前界面
            location.reload()
          }else {
            $("#register-password-err").html(resp.errmsg)
            $("#register-password-err").show()
          } } } )
    

    3.2 新闻点击排行

    • 查询新闻表按照点击次数排序,转化成字典列表后返回数据并渲染模板

      news_list = News.query.
      order_by(News.clicks.desc()).limit(constants.CLICK_RANK_MAX_NEWS)
      

      模板展示数据

      <ul class="rank_list">
        {% for news in data.click_news_list %}
          <li><span class="{{ loop.index0 | indexClass }}">{{ loop.index }}</span><a href="#">{{ news.title }}</a></li>
        {% endfor %}
      </ul>
      

    3.3 新闻分类展示(同租房城区列表,略)

    categories = Category.query.all()
    

    3.4 新闻列表实现

    • 获取第几页page,每页数量per_page,分类category_id参数

    • 检验参数后,查询数据并分页,后端有配合的js代码,详情见下文

      filters = []
      # 如果分类id不为1,那么添加分类id的过滤
      if category_id != "1":  
        filters.append(News.category_id == category_id)
      paginate = News.query.filter(*filters).
      order_by(News.create_time.desc()).paginate(page, per_page, False)
      
    • 返回json数据,包括 total_pages,current_page,和新闻数据

    3.5 定义一个装饰器判断用户是否登录

    def user_login_data(f):
    #装饰器会改变被装饰函数的端点名称,采用此手段去除影响
      @functools.wraps(f)
      def wrapper(*args, **kwargs):
        # 获取到当前登录用户的id
        user_id = session.get("user_id")
        # 通过id获取用户信息
        user = None
        if user_id:
          from info.models import User
          user = User.query.get(user_id)
        g.user = user
        return f(*args, **kwargs)
      return wrapper
    

    3.6 详情页新闻展示

    • 获取news_id,查询news对象
    • 和 user_info数据,新闻点击列表数据一同传给详情页模板进行渲染

    3.7 收藏和取消收藏(前后端不分离的开发方式耦合度高,在这里体现的很明显,新闻详情页这个要渲染的模板中涉及的数据就很多,包括:is_collected,user_info,news,click_news_list,is_followed,comments等,要挤在一个视图函数中)

    • 判断用户登录,获取 news_id,action

    • 检验参数,获取新闻对象

    • 根据action选择:append(news),还是remove(news),返回的是json数据。

      if action == "collect":
      user.collection_news.append(news)
      else:
      user.collection_news.remove(news)
      

    3.8 新闻评论

    • 判断用户是否登录,获取参数 news_id,comment_str,parent_id
    • 检验参数,重点是判断新闻是否存在
    • 创建评论模型类,保存数据提交,并且通过json返回评论数据(新闻详情页模板渲染时需要传评论数据,见下文)。

    3.9 评论列表

    • 获取news_id,取出新闻所有评论对象

      comments = Comment.query.filter(Comment.news_id == news_id).
      order_by(Comment.create_time.desc()).all()
      
      comment_list=[item.to_dict() for item in comments]
      
    • 放入传给模板文件的data中,模板内判断是否有父评论

      {% if comment.parent %}
            <div class="reply_text_con fl">
              <div class="user_name2">
                {{ comment.parent.user.nick_name }}</div>
            <div class="reply_text">
               {{ comment.parent.content }}
             </div>
            </div>
      {% endif %}
      

    3.9 点赞

    • 前端获取 comment_id,news_id,action参数,参数检验和用户登录检验

    • 根据comment_id取出评论对象,根据 action 决定增加 comment_like 对象还是 remove(comment_like)对象,并对 comment.like_count 进行相应加减。以下是前端对应代码:

      success: function (resp) {
            if (resp.errno == "0") {
              // 更新点赞按钮图标
              if (action == "add") {
                // 代表是点赞
                $this.addClass('has_comment_up')
              }else {
                $this.removeClass('has_comment_up')
              }
            }else if (resp.errno == "4101"){
              $('.login_form_con').show();
            }else {
              alert(resp.errmsg)
            }
          }
      
    • 取出该新闻下所有 评论对象,继而通过 评论id 取出 所有点赞对象,继而生成 被用户点赞的评论的id 组成的列表

      comments = Comment.query.
      filter(Comment.news_id == news_id).
      order_by(Comment.create_time.desc()).all()
      
      comment_ids = [comment.id for comment in comments]
      
      comment_likes = 
      CommentLike.query.filter
      (CommentLike.comment_id.in_(comment_ids),                            CommentLike.user_id == g.user.id).all()
      
      comment_like_ids = 
      [comment_like.comment_id for comment_like in comment_likes]
      
    • 先假定用户没有点赞评论,再判断该评论id是否在comment_like_ids,如果在如下操作,和用户收藏是相似的套路,而且都需要传给模板渲染。

      comment_dict["is_like"] = True
      
  4. 个人中心

    4.1 修改用户信息

    • 后端更新并保存数据,更改状态保持即可

    • 前端成功回调函数,多处name都要改变

      if (resp.errno == "0") {
      // 更新父窗口内容
      $('.user_center_name', parent.document).html(params['nick_name'])
      $('#nick_name', parent.document).html(params['nick_name'])
      $('.input_sub').blur()
      }else { 
      alert(resp.errmsg) }
      

    4.2 用户收藏数据加载

    • 先获取页数参数,设定默认值

      collections = []
      current_page = 1
      total_page = 1
      
    • 进行分页数据查询,给默认数据赋值,处理收藏数据列表后返回数据。前端展示分页界面的代码如下,内部的 currentPage,totalPage 是全局变量。

      paginate = user.collection_news.paginate(p,constants.USER_COLLECTION_MAX_NEWS, False)
      collections = paginate.items
      current_page = paginate.page
      total_page = paginate.pages
      
      $(function() {
            $("#pagination").pagination({
              currentPage: {{ data.current_page }},
              totalPage: {{ data.total_page }},
              callback: function(current) {
                window.location.href = "/user/collection?p=" + current
              }
            });
          });
      

    4.2 用户发布新闻

    • 获取新闻分类的数据,但要pop掉 ‘最新’

      categories = Category.query.all()
      categories_dicts = [categorie.to_dict() for categorie in categories]
      categories_dicts.pop(0)
      
    • 获取前端提交的数据:title,digest,content,index_image,category_id,参数检验

    • 读取图片上传七牛云,初始化新闻对象存储数据,设置 news.status=1

    4.3 用户新闻列表(略)

  5. 其他

    5.1 关注和取消关注

    • 获取 user_id,action 参数,获取发布新闻的用户对象

    • 根据action采取执行不同语句,返回json数据

      if action == "follow":
      if target_user.followers.filter(User.id == g.user.id).count() > 0:
      return jsonify(errno=RET.DATAEXIST, errmsg="当前已关注")
      target_user.followers.append(g.user)
      else:
      if target_user.followers.filter(User.id == g.user.id).count() > 0:
      target_user.followers.remove(g.user)
      
      // 关注当前新闻作者
      $(".focus").click(function () {
        var user_id = $(this).attr('data-userid')
        var params = {
          "action": "follow",
          "user_id": user_id
        }
        $.ajax({
          url: "/news/followed_user",
          type: "post",
          contentType: "application/json",
          headers: {
            "X-CSRFToken": getCookie("csrf_token")
          },
          data: JSON.stringify(params),
          success: function (resp) {
            if (resp.errno == "0") {
              // 关注成功
              var count = parseInt($(".follows b").html());
              count++;
              $(".follows b").html(count + "")
              $(".focus").hide()
              $(".focused").show()
            }else if (resp.errno == "4101"){
              // 未登录,弹出登录框
              $('.login_form_con').show();
            }else {
              // 关注失败
              alert(resp.errmsg)
            } } }) })
      

    5.2 其他用户界面

    • 获取其他用户id,检验参数后,取出该用户对象

    • 判断是否关注

      is_followed = False
        if g.user:
          if other.followers.filter(User.id == user.id).count() > 0:
            is_followed = True
      
    • 其他用户新闻列表(略)

  6. 后台

    6.1 管理员登录

    • GET请求获取模板页面后,前端提交两个参数 username,password

    • 通过username取出用户对象,做密码验证

      if not user.check_passowrd(password):
          return render_template('admin/login.html', errmsg="密码错误")
      
    • 同时验证用户是否是管理员

      {% if errmsg %}
      <div class="error_tip" style="display: block">{{ errmsg }}</div>
      {% endif %}
      
    • 使用端点名重定向到后台主页

      return redirect(url_for('admin.admin_index'))
      

    6.2 用户统计

    • 获取到本月第1天0点0分0秒的时间对象,然后查询最后一次登录比其大的所有数据

      now = time.localtime()
      mon_begin = '%d-%02d-01'%(now.tm_year, now.tm_mon)
      mon_begin_begin = datetime.strptime(mon_begin, '%Y-%m-%d)
      mon_count =User.query.
      filter(User.is_admin == False, User.create_time >= mon_begin_date).count()
      
    • 获取到当日0点0分0秒时间对象,然后查询最后一次登录比其大的所有数据

      day_begin = '%d-%02d-%02d' % (now.tm_year, now.tm_mon, now.tm_mday)
      day_bedin_date = datetime.strptime(day_begin, '%Y-%m-%d)
      day_count = User.query.
      filter(User.is_admin == False, User.create_time >= day_begin_date).count()
      
    • 图表查询:遍历查询数据每一天的数据(当前天数,减去某些天)

      now_date = datetime.strptime(datetime.now().strftime('%Y-%m-%d'), '%Y-%m-%d')
      active_date = []
      active_count = []
      for i in range(0, 31):
      begin_date = now_date-timedelta(days=i)
      end_date = now_date-timedelta(days=(i-1))
      active_date.append(begin_date.strftime('%Y-%m-%d'))
      count = 0
      count = User.query.filter(User.is_admin == False, User.last_login >= day_begin, User.last_login < day_end).count()
      active_count.append(count)
      active_date.reverse()
      active_count.reverse()
      

    6.3 用户列表(略)

    6.4 新闻审核列表

    • 新闻列表的关键字搜索的实现,后端接受了参数 page,keywords,增加过滤条件

      filters = [News.status != 0]
      if keywords:
      # 添加关键词的检索选项
      filters.append(News.title.contains(keywords))
      
    • 查询出新闻分页对象,返回前端即可

    6.5 新闻审核详情

    • 根据参数action的值修改新闻对象的status值,如果status=-1,需要增加reason

      // 获取到所有的参数,尤其是reason
      $(this).serializeArray().map(function (x) {
      params[x.name] = x.value; });
      
      if (resp.errno == "0") {
              // 返回上一页,刷新数据
              location.href = document.referrer; }
      

    6.6 用户新闻编辑

    • 编辑详情界面数据,GET请求返回分类数据,新闻对象数据,要判断当前新闻属于哪个分类

      categories = Category.query.all()
        categories_li = []
        for category in categories:
          c_dict = category.to_dict()
          c_dict["is_selected"] = False
          if category.id == news.category_id:
            c_dict["is_selected"] = True
          categories_li.append(c_dict)
        # 移除最新分类
        categories_li.pop(0)
      
    • 前端编辑提交,后端接受参数:news_id,title,digest,content,index_image,category_id,验证后,给news对象添加数据,保存,返回json数据。前端发送数据前需要的处理如下:

      beforeSubmit: function (request) {
              // 在提交之前,对参数进行处理
              for(var i=0; i<request.length; i++) {
                var item = request[i]
                if (item["name"] == "content") {
                  item["value"] = tinyMCE.activeEditor.getContent()
                } } }
      

    6.7 新闻分类管理(略)

  7. 注意点:

  • 当前端需要一次性展示页面效果,所需的数据不会动态变化使用模板渲染数据

  • 当前端需要动态展示页面数据和处理事件,所需数据 根据情况变化时,使用ajax或者其他方式向后端发送异步请求。

  • 先行假定数据一个安全值,然后推翻,赋予数据真实值,做到安全性的套路。

    is_followed = False
      if g.user:
        if other.followers.filter(User.id == user.id).count() > 0:
          is_followed = True