新经资讯是一个包含新闻首页,新闻详情,用户中心和后台管理等模块的Flask项目
项目框架的搭建
- 包含配置信息:mysql,redis,session等
- 工厂函数产出多种配置的app,数据库对象db,redis_store等
- 日志的记录
- 第三方库:云通讯,七牛云
- 设置的常量,自定义状态码,commons,models等
- flask_script,flask_migrate等
根据需求分析E-R图,构建模型类
![](D:\Users\Liu xiangyu\Desktop\新经资讯E-R图.png)
新闻首页和详情页模块
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.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.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.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 新闻分类管理(略)
注意点:
当前端需要一次性展示页面效果,所需的数据不会动态变化使用模板渲染数据
当前端需要动态展示页面数据和处理事件,所需数据 根据情况变化时,使用ajax或者其他方式向后端发送异步请求。
先行假定数据一个安全值,然后推翻,赋予数据真实值,做到安全性的套路。
is_followed = False if g.user: if other.followers.filter(User.id == user.id).count() > 0: is_followed = True