moments 项目

moment项目需求

相关技术栈:redis、golang、MySQL、linux

做一个看板任务(照着微信朋友圈来,不过所有用户全部看成朋友即可)

  1. 用户可以登录,可以保持登录状态
  2. 用户可以发布一个想法,包含一段文字,0-4张图片(保存图片链接即可)
  3. 用户可以查看大家发布的说说,按时间顺序从新展示,需分页,一页20个
  4. 用户可以评论想法,也可以删除自己的评论
  5. 用户可以点赞/取消点赞想法
  6. 消息通知,点赞回复要通知所有和说说相关的人

不做界面,规划好接口,数据库,且完成即可
数据库尽可能满足 一对一 一对多 多对多 关系,不要求字段冗余
使用HTTP协议,框架自选(iris,beego等都行)

进度安排:

  1. 设计好数据库
  2. 规划好接口
  3. 做好登录认证
  4. 完成接口

数据库实现

数据库E-R图怎么画?

实体,属性,关系;实体就是一个个对象,比如猫;属性就是实体所有的某个属性,比如猫的性别;关系就是实体和实体之间或者实体内部之间的关系。
在ER图中,矩形代表实体,椭圆代表属性,菱形代表关系,各个形状之间用线段连接。

在本项目中,实体有

  1. 内容发布者
  2. 朋友圈发布项
  3. 朋友圈的评论项

内容发布者对应属性:

  • 用户ID(系统内部)(主键)
  • 用户姓名(界面展示)
  • 用户密码

朋友圈标志属性:

  • 发布ID(每个发布者发布朋友圈时调用,自增1,本身具有时间顺序特性)(主键)
  • 发布者用户ID(外键)
  • 创建时间(显示在朋友圈下方)
  • 照片列:照片url;照片url;照片url;照片url
  • 配文字符串

评论属性:(评论归属于单个用户,并且此用户为接收者,而不是发布者)

  • 用户ID(评论创建者,用来检索对应的用户名,以展示用户评论)(外键)
  • 所属朋友圈,即发布ID(和用户ID组合作为主键)(外键)
  • 评论创建时间(按时间顺序显示评论)
  • 评论内容字符串
  • 点赞与否

数据库设计思路

做一个看板任务(照着微信朋友圈来,不过所有用户全部看成朋友即可)

  1. 用户可以登录,可以保持登录状态
  2. 用户可以发布一个想法,包含一段文字,0-4张图片(保存图片链接即可)
  3. 用户可以查看大家发布的说说,按时间顺序从新展示,需分页,一页20个
  4. 用户可以评论想法,也可以删除自己的评论
  5. 用户可以点赞/取消点赞想法
  6. 消息通知,点赞回复要通知所有和说说相关的人

不做界面,规划好接口,数据库,且完成即可
数据库尽可能满足 一对一 一对多 多对多 关系,不要求字段冗余
使用HTTP协议,框架自选(iris,beego等都行)

微信朋友圈设计架构

微信朋友圈的数据有四个核心的表:

  1. 发布表,发布表记录了来自所有用户的所有Feed,比如一个用户发布了几张图片,每张图片的URL,图片在CDN里的URL,它有哪些元属性,谁可以看,谁不可以看等等。
  2. 相册,相册是每个用户独立的,记录了该用户所发布的所有内容,包括了与图片相关的文字,和无配图文字。
  3. 评论, 评论就是针对某个具体发布的朋友评论和点赞操作。
  4. 时间线,所谓“刷朋友圈”,就是刷时间线,就是一个用户所有的朋友的发布内容。

微信朋友圈的工作流程概述

  1. 比如有两个用户A和B, A和B各自都有各自的相册(可以理解为A和B各自的朋友圈内容),可能在同一台服务器上,也可能在不同服务器上。现在A上传了一张图片到自己的朋友圈。上传图片不经过微信服务器,而是直接上传到最近的CDN节点去,所以非常快。图片上传到CDN后,小王的微信客户端会通知,这里有一个新的发布(比如叫K2),这个发布的图片CDN URL是什么,谁能看这张图片等等此类的元数据。来把这些数据写入发布表中。

  2. 在发布表写完之后,会把K2的发布索引到A的相册表中,所以相册表记录得就是每个内容索引指针,相册表写好后会触发一个批处理动作,这个动作就是去跟A的每个好友说,A有一个新的发布,请把这个发布插入到每个好友的时间线里去。

  3. 现在B上朋友圈了,而B是A的一个好友,B拉自己的时间线的时候,时间线会获得K2的新发布通知,然后B的微信客户端就会取根据K2的元数据去获取发布表中的一些信息比如:CDN URL,把图片拉到本地。

在这个过程中,发布是很重要的,因为一方面要写一个自己的数据副本,并写入自己的时间线,还要把这个副本的指针插到所有好友的时间线里面去,如果一个用户有几百个好友的话,这个过程会比较慢一些。这是一个单数据副本写扩散的过程。但是相对应的,读取就很简单。每个用户只需要读取自己的时间线这一个动作就行,不需要去遍历所有的好友相册表。这个是否有一定得数据冗余??

参考文献:

  1. 朋友圈的设计及实现
  2. 微信与朋友圈后台架构
  3. 微信朋友圈数据库模式如何设计的?
  4. 微信与朋友圈后台架构的讲解
  5. 控制朋友圈好友状态可见性的数据表设计

数据交互逻辑

用户登录

匹配password字段—>若成功,登录操作(http协议)进入看板界面,推送Feed流信息,若失败,拒绝访问,“密码错误,请重新输入密码”,超时锁定。

刷朋友圈

登录成功—>推送publish,按publish_id从大到小排列,展示publish_id最大的20个publish。
刷朋友圈的行为即为刷取多个朋友圈信息(publish)对于单个publish:

  • 获取创建时间—>拉取create_time在左下方显示;
  • 获取照片—>拉取picture表中,publish_id对应的pic_url;
  • 获取朋友圈文字—>拉取caption;
  • 获取评论—>拉取comment表中,publish_id对应的comment,根据comment_id进行从大到小排序,根据usr_id,检索出usr_name(即评论人),拉取comments,和comment create_time(整体显示情况即为:评论人,评论,评论创建的时间);
  • 获取点赞信息—>拉取like表中,publish_id对应的like项,并根据like_id,从大到小排序(整体显示情况即为:点赞人A,点赞人B,点赞人C…)

发布朋友圈

创建一个新的publish表项,publish_id自增1,根据当前用户,填入当前usr_id,录入当前时间到create_time,将朋友圈文字信息,录入到caption字段中;

若有照片,每个照片创建一个picture表项,录入pic_url和当前publish_id,pic_id自增1

评论朋友圈

创建一个新的comment表项,comment_id自增1,根据当前用户,填入当前usr_id,录入当前时间到create_time,将评论文字信息,录入到comments字段中,并录入当前评论的publish_id;

创建Notice,根据当前评论的publish_id,检索,当前Notice表中,具有同样publish_id的Notice,查看其中的from_id和to_id,即为所有相关的用户,创建对应的Notice,notice_id自增1,publish_id即为当前评论的publish_id,type为0(0为评论,1为点赞),from_id为当前评论的用户,to_id依次为检索到的所有用户ID,status为0(0未读,1已读);

点赞朋友圈

创建一个新的like表项,like_id自增1,根据当前用户,填入当前usr_id,填入当前点赞的publish_id;
创建Notice,根据当前评论的publish_id,检索,当前Notice表中,具有同样publish_id的Notice,查看其中的from_id和to_id,即为所有相关的用户,创建对应的Notice,notice_id自增1,publish_id即为当前评论的publish_id,type为1(0为评论,1为点赞),from_id为当前评论的用户,to_id依次为检索到的所有用户ID,status为0(0未读,1已读);

推送评论通知基本逻辑

  • 接收通知的用户,通知详情需要主动获取
  • 通知的主动推送,本质为当前用户定时,每秒查看,有多少条未读通知 (*)
  • 根据Notice表,主动获取通知,定时获取to_id与当前用户usr_id相符,且status字段为0(未读)的Notice。通知格式为“您有N条消息未读”。并同时存储这些Notice的notice_id
  • 当用户点击通知,读取通知时,检索存储的notice_id,将这些Notice的status字段置为1,并根据type获知通知类型,若type=0为评论,则查找comment表项,查找对应publish_id中usr_id=from_id的comments字段,获取评论详情(from_id+comments),上方要同时拉取对应publish_id的朋友圈详情(usr_id+pic+caption+create_time);若type=1为点赞,则拉取对应publish_id的朋友圈详情,并显示,”(from_id) like this”,上方要同时拉取对应publish_id的朋友圈详情(usr_id+pic+caption+create_time)

通知的几种特殊情况的考虑

  • (未评论用户)取消点赞后,需要去掉该用户的Notice
  • (未点赞用户)删除评论后,需要去掉该用户的Notice
  • 阅读过通知后,通知需要保留,将状态置为已读,当后续还有人评论时,可以根据Notice表自动维护之后的通知。
  • 每一次的通知都可以直接通过上一次通知进行维护更新。第一步,找到对应的publish_id中notice_id的最大的一项,找出对应的from_id,即找出了,本朋友圈上一次的通知,是从from_id,通知到了此用户的所有to_id。即所有最新的相关用户群,则更新的Notice为,from_id=当前usr_id,to_id=上一次的from_id+上一次的所有的to_id。
  • 初次生成的通知会被立刻调用,所以也可以直接在生成的时候,直接推送。 (*)

MySQL设计规范

详情,点击标题跳转页面↑


接口设计

接口基础函数

Prepare

Beego框架中,Controller的Prepare函数都会自动在调用其之前先被调用。
所以Prepare函数,被我用来写验证相关的代码。

①假如当前是在进行非登录操作,则需要验证,session_id,若为空,则未登录,跳转到登录操作页面
②若不为空,则在redis中进行比对,看是否存在当前用户的session_id,K-VOUSMEVOYEZ结构为“K:session_id,VOUSMEVOYEZ:usr_name”,若查找当前session_id,得到的用户为空,则未登录,跳转到登录操作页面;得到的用户名非空,还需要验证一下用户表中是否存在该用户,不存在则显示为仿冒用户
③若不存在session_id的问题,执行完Prepare函数仍未被跳转到登录页面,则继续执行Controller

History

一个跳转函数,用来控制,当出现错误或者验证安全的问题时,及时做出反馈,当出现系统性错误时,停止系统的运行,当出现验证问题时,及时跳转到所需页面,如登录页面等。
msg:用来描述当前问题,并写入到跳转页面
url:用来描述跳转链接,当为空时,停止运行当前controller

Finish

Beego框架中,任何Controller的Finish函数都会自动在调用后,最后时刻被调用,所以Finish函数,被我用来写Session的控制。

①假如当前是在进行非登录登出操作,需要验证后,为当前Session_id,续期30分钟
②假如是登录操作,需要为当前用户创建一个session_id,存到redis中,并发送给客户端


登录接口

Login

路由:”/admin/login”
用户登录输入用户名密码,以Json格式发送至客户端,若存在Session,且和服务器内Session_id相符,则直接验证通过

1
2
3
4
5
//UsrInfo 用户评价时提交的表单
type UsrInfo struct {
UsrName string `form:"usr_name"`
Password string `form:"password"`
}

输入:Body包含用户名密码的Json格式文档,或其他包含Session_id的Json格式文档
通过解析Json,解析出用户名,密码,和Session_id:
①假如Session_id非空,且在Redis中存在,则更新Redis,直接进入下一步;
②假如Session_id非空,但在Redis中不存在,则登录超时,要求重新登录,进入登录界面
③假如Session_id为空,则进入登录界面,解析客户端发送过来的用户名密码,若用户名密码匹配,则登录成功,并生成一个Session记录,保存在Redis,同时将Session_id作为输出发送给客户端。否则直接返回“用户名或密码错误”,要求重新登录

1
2
3
4
5
6
//LoginResult 传递登录结果
type LoginResult struct {
Result bool `json:"result"`
UsrID int `json:"usr_id"`
SessionID string `json:"session_id"`
}

输出:验证结果(true/false)+ session_id + usr_id(给客户端保存)

Logout

路由:”/admin/login”
用户退出登录的时候,需要清除掉当前用户的Session_id。然后跳转到登录页面。

输入:session_id

1
2
3
4
5
//LogoutFeedback 退出登录反馈信息
type LogoutFeedback struct {
Result bool `json:"result"`
Info string `json:"info"`
}

输出:返回到登录界面


朋友圈接口

PostMoment(发布朋友圈)

分析:客户端发布朋友圈,通过表单发送给后台,返回一个publish_id,和发布结果;发布朋友圈时,所需要的信息是,用户名,文字信息,和图片,由此构建表单结构体

路由:”/feed/post”

1
2
3
4
5
6
//PushMoment is 发送朋友圈时提交的表单
type PushMoment struct {
UserName string `form:"usr_name"`
Caption string `form:"caption"`
PicList []string `form:"pic_list"`
}

输入:朋友圈所需信息的表单。

1
2
3
4
5
//PostResult 发布朋友圈后的反馈结果
type PostResult struct {
Result bool `json:"result"`
PublishID int64 `json:"publish_id"`
}

输出:true or false 若为true 还需要返回一个当前创建成功后,数据库中publish表的publish_id,方便后期加评价和删除朋友圈。

ReadMoment(刷朋友圈)

分析:由于本需求中,所有人都为朋友,所以所有人的展示结果只跟发送展示请求的时间有关(当做一个锚点),读取朋友圈时,不要读取最新的朋友圈信息,即只刷取,锚点以前,请求时的当前数据库数据。如若不然,会导致分页重复,刷新朋友圈为动态操作(和刷朋友圈区分开,刷朋友圈是静态操作)。最新的朋友圈和评论产生时,只轮询读取到通知,而不拉取最新的朋友圈信息。当用户主动刷新的时候,再读取最新的信息。

路由:”/feed/circle”
用户登录成功跳转至朋友圈界面,查询当前publish列表,记录此时最新的publish(即为Publish_id最大值),记录为publishAnchor,通过锚点进行分页展示。
通过publish_id排列,查找20个最新的朋友圈,将所有的数据打包发送出去。

输入:publish_index(数据类型int,读取朋友圈信息时publish_id的开始处)

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
//MomentFeed 朋友圈数据流
type MomentFeed struct {
Moments []models.OneMoment `json:"Moments"`
PublishID int `json:"publish_id"`
}

//OneMoment is 单个朋友圈的Json格式
type OneMoment struct {
PublishID int `json:"publish_id"`
UserName string `json:"usr_name"`
Caption string `json:"caption"`
PicList []string `json:"pic_list"`
CreateTime string `json:"create_time"`
LikeList []string `json:"like_list"`
Comments CommentList `json:"comments"`
}

//OneComment is 单个评论传输时的Json格式
type OneComment struct {
CommentID int `json:"comment_id"`
UsrName string `json:"usr_name"`
CommentCaption string `json:"comment_caption"`
CreateTime string `json:"create_time"`
}

//CommentList is 评论列表,Json格式
type CommentList []OneComment

输出:锚点后的20条朋友圈信息(JSON)。创建一个朋友圈整体信息的结构体。

DelMoment(删除朋友圈)

根据前端发送来的pubish_id,进行对当前朋友圈的一系列删除,包括,相册表,评论表,点赞表和通知表。
路由:”/feed/del”

输入:publish_id

1
2
3
4
5
//DelFeedInfo 删除朋友圈后的反馈信息
type DelFeedInfo struct {
Result bool `json:"result"`
Info string `json:"info"`
}

输出:反馈信息,true or false


评论接口

CreateComment(发布评论接口)

根据前端发送来的评论信息,建立数据库信息,因为前段发来的评论信息,要有,评论相关文字,评论所属对象,和评论所属朋友圈,故建立表单信息进行接收。

路由:”/feed/comment/create”

1
2
3
4
5
6
7
//OneComment 用户评价时提交的表单
type OneComment struct {
UsrID int `form:"usr_id"`
UsrName string `form:"usr_name"`
CommentCaption string `form:"comment_caption"`
PublishID int `form:"publish_id"`
}

输入:form

1
2
3
4
5
6
//CreateCommentFeedback 评论创建反馈
type CreateCommentFeedback struct {
CommentID int64 `json:"comment_id"`
CreateResult bool `json:"create_result"`
NoticeStatus bool `json:"notice_status"`
}

输出:反馈信息,返回告诉客户端,该评论的comment_id,以及评论成功与否,通知是否已经建立等确认信息。

DelComment(删除评论接口)

给出对应的comment_id,删除所有的相关信息。改动涉及:comment表和notice表

路由:”/feed/comment/del”

输入:comment_id

1
2
3
4
5
6
//DelCommentFeedback 评论创建反馈
type DelCommentFeedback struct {
DelResult bool `json:"del_result"`
DelNoticeResult bool `json:"del_notice_status"`
Info string `json:"info"`
}

输出:反馈信息,返回告诉客户端,评论删除与否,评论涉及的通知信息删除与否。


点赞接口

LIke(点赞)

点赞过程中,需要在点赞表中建立一条新信息,相应的,点赞通知的建立,需要通过CreateNotice遍历的在Notice表中建立多条信息。所以需要usr_id ,usr_name ,publish_id 三个信息,以上信息以表单形式传递。

路由:”/feed/like/create

1
2
3
4
5
6
//Like is 前端大赞操作发送来的表单
type Like struct {
UsrID int `form:"usr_id"`
UsrName string `form:"usr_name"`
PublishID int `form:"publish_id"`
}

输入:点赞表单:Like结构体

1
2
3
4
5
6
//CreateLikeFeedback 创建点赞后给出的反馈信息
type CreateLikeFeedback struct {
LikeID int `json:"like_id"`
CreateResult bool `json:"create_result"`
NoticeStatus bool `json:"notice_status"`
}

输出:反馈信息,返回告诉客户端,创建的点赞的ID是多少,创建结果如何,通知是否创建成果的信息。

CancelLike(取消点赞)

取消点赞,得到取消点赞的对应like_id即可,删除like表中对应的信息,并同时删除对应的通知信息。

路由:”/feed/like/cancel

输入:like_id

1
2
3
4
5
6
//CancelLikeFeedback 取消点赞反馈
type CancelLikeFeedback struct {
CancelResult bool `json:"cancel_result"`
DelNoticeResult bool `json:"del_notice_result"`
Info string `json:"info"`
}

输出:取消的结果,删除通知的结果,以及相关的反馈信息。以便后期对应做出维护。


通知接口

通知被设计成,创建通知,删除通知为基本函数,穿插在点赞和评论的基本操作当中,并非一个单独的接口,而获取通知被设计成一个接口,目的就是能够随时获得最新的通知,能随时调用这个接口,读取操作可以作为常用接口,而增删通知则不开放为一个接口来调用。

GetNotice(获得通知)

获得通知时无需发送多余的信息,直接通过Session即可
路由:feed/notice
输入:session_id (从redis中获得usr_name)
输出:通知信息。通知信息所需要的基本信息可以分析,通知信息的前端展示:
①朋友圈有更新的通知:如果有新的朋友圈,一个红点,如果没有,则没有红点;
②自己的朋友圈被评论的通知:XX(usr_name)评价了您的“XXX”(某条朋友圈的部分文字信息);
③自己参与的别人的朋友圈被其他人评论的通知:XX(usr_name)评价了“XXX”(某条朋友圈的部分文字信息);
④自己的朋友圈被点赞的通知:XX(usr_name)点赞了您的“XXX”(某条朋友圈的部分文字信息);
⑤自己参与的别人的朋友圈被其他人点赞的通知:XX(usr_name)点赞了“XXX”(某条朋友圈的部分文字信息);
前端展示如上五种通知,则需要知道的信息有:

  1. 该条通知,是通知有新的朋友圈还是新的评论还是有新的点赞?
  2. publish表单是否有新的内容
  3. 评论时,是谁评论了哪条朋友圈
  4. 点赞时,是谁点赞了哪条朋友圈

即为,publish_id,from_id,notice_type至于这条朋友圈的所属者是不是通知对象本身,可以由客户端自行判断。
因为通知的结构体如下:

1
2
3
4
5
6
7
8
9
//OneNotice 单个通知信息
type OneNotice struct {
NoticeID int `json:"notice_id"`
PublishID int `json:"publish_id"`//指向对应的朋友圈
NoticeType int `json:"notice_type"`//0为评论,1为点赞
CommentID int `json:"comment_id"`
LikeID int `json:"like_id"`
FromUserName string `json:"from_user_name"`
}

当客户端接收到的publish_id为0时,该条Notice意思是:有新的朋友圈信息。
当客户端接收到的publish_id不为0时,(如23),from_user_name:liuxing1,notice_type:1,该条信息的意思是:23号朋友圈有来自liuxing1的点赞
当客户端接收到的publish_id不为0时,(如23),from_user_name:liuxing1,notice_type:0,该条信息的意思是:23号朋友圈有来自liuxing1的评论

1
2
3
4
//NoticeList 发送给用户的通知信息
type NoticeList struct {
Notices []OneNotice `json:"notices"`
}

故,以上通知列,可以告知用户一切相关的通知信息,点击这些信息的时候,再调取具体的通知信息给用户呈现。

GetInfo(根据通知,来获取相关信息的更新)

路由:feed/getinfo
以上五种通知,通知1不会单独呈现,只需要用小红点告知用户有最新动态,刷新朋友圈即可。通知2,3,4,5则会单独呈现,2,3点选的时候,会在上方展示相关朋友圈,下方会显示所有的点赞评论信息,即,将当前发生最新动态的朋友圈的最新状态整个从服务端拉取过来。点赞信息的更新类似。

输入:通过“session_id”可以得知usr_id,usr_name。只需要publish_id即可进行信息的更新。

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
//NewMoment 回传的最新单条朋友圈详情
type NewMoment OneMoment

//OneMoment is 单个朋友圈的Json格式
type OneMoment struct {
PublishID int `json:"publish_id"`
UserName string `json:"usr_name"`
Caption string `json:"caption"`
PicList []string `json:"pic_list"`
CreateTime string `json:"create_time"`
LikeList []string `json:"like_list"`
Comments CommentList `json:"comments"`
}

//CommentList is 评论列表,Json格式
type CommentList []OneComment

//OneComment 用户评价时提交的表单
type OneComment struct {
UsrName string `json:"usr_name"`
CommentCaption string `form:"comment_caption" json:"comment_caption"`
PublishID int `form:"publish_id" json:"-"`
CommentID int `json:"comment_id"`
CreateTime string `json:"create_time"`
}

输出:单条朋友圈的最新状态。

实现所需知识树

Beego框架的使用

实例
samples/WebIM at master · beego/samples · GitHub

Session

由于需要保持登录状态,所以这里需要用到Session控制
Go基础学习记录之如何在Golang中使用Session - Go语言中文网 - Golang中文社区
Session control - beego: simple & powerful Go app framework

Redis

Redis 教程

遇到的坑

Json解析首字母一定要大写

1
2
3
4
5
6
//CreateCommentFeedback 评论创建反馈
type CreateCommentFeedback struct {
CommentID int64 `json:"comment_id"`
CreateResult bool `json:"create_result"`
NoticeStatus bool `json:"notice_status"`
}

编码JSON的时候,json:”-“可以让其不参与编码,不然还是会编码,不过不会更名。

1
PublishID      int    `form:"publish_id" json:"-"`

若为

1
PublishID      int    `form:"publish_id"`

编码的时候,还是会有这个字段,并且,显示的是,PublishID:XXX

redis的操作问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
Cli.Set("liuxing1", "aaaaa", 30*time.Minute)
a := Cli.Get("liuxing1")
fmt.Println(a.Val())
fmt.Println(a.String())
b := Cli.Del("liuxing")
fmt.Println(b.Val())
c := Cli.Get("liuxing1")
fmt.Println(c.Val())
fmt.Println(c.Val()=="")
fmt.Println(c.Err())
b = Cli.Del("liuxing1")
fmt.Println(b.Val())
c = Cli.Get("liuxing1")
fmt.Println(c.Val())
fmt.Println(c.Val()=="")
}

输出:

1
2
3
4
5
6
7
8
9
aaaaa
get liuxing1: aaaaa
0
aaaaa
false
<nil>
1

true