415 unsupported media type

15

在我们的即时消息应用程序中,消息显示为两个参与者之间的一堆对话。尼古拉斯·帕拉达(作者)

本文是本系列的第三篇。

第一篇:模式第二篇:OAuth

在我们的即时消息应用程序中,消息显示为两个参与者之间的一堆对话。如果你想开始一个对话,你应该向应用程序提供你想与之对话的用户,当对话被创建时(如果之前不存在的话),你可以向它发送一条消息。

在前端,我们可能希望显示最近的对话列表。对话的最后一条消息以及另一个参与者的姓名和头像都显示在这里。

在这篇文章中,我们将编写一些端点来完成诸如“创建会话”、“获取会话列表”和“查找单个会话”等任务。

首先,将下面的route添加到主函数main()。

路由器。handle func(& # 34;邮政& # 34;, "/API/conversation & # 34;,要求JSON(guard(createConversation)))路由器。handle func(& # 34;获取& # 34;, "/API/conversation & # 34;,guard(getConversations))路由器。handle func(& # 34;获取& # 34;, "/API/conversations/:conversation id & # 34;,guard(getConversation))三个端点都需要认证,所以我们会使用guard()中间件。我们还将构建一个新的中间件来检查所请求的内容是否是JSON格式。

JSON 请求检查中间件

func requireJSON(处理程序http。HandlerFunc) http。HandlerFunc {return func(w http。ResponseWriter。request){ if CT:= r . header . get(& # 34;内容类型& # 34;);!弦乐。HasPrefix(ct,& # 34;应用程序/JSON & # 34;){http。错误(w,& # 34;需要应用程序/json的内容类型& # 34;,http。statusunsupported Media Type)return } handler(w,r)}如果请求不是JSON格式,将返回415不支持的媒体类型错误。

创建对话

type Conversation struct { id string ` JSON:& # 34;id & # 34` other participant * user ` JSON:& # 34;其他参与者& # 34;` last Message * Message ` JSON:& # 34;lastMessage & # 34` hasunreadmessages bool ` JSON:& # 34;hasUnreadMessages & # 34就像上面的代码一样,对话保留了对另一个参与者和最后一条消息的引用,并且有一个bool类型的字段来告诉是否有未读消息。

类型消息结构{ ID string ` JSON:& # 34;id & # 34` content string ` JSON:& # 34;内容& # 34;` userid string ` JSON:& # 34;-"` conversation id string ` JSON:& # 34;conversationID,omitempty & # 34` createdattime . time ` JSON:& # 34;createdAt & # 34` mine bool ` JSON:& # 34;我的& # 34;` receive id string ` JSON:& # 34;-"我们将在下一篇文章中介绍与消息相关的内容,但是因为我们在这里需要它,所以我们首先定义消息结构。这些字段中的大多数都与数据库表一致。我们需要使用Mine来确定消息是否属于当前经过身份验证的用户。一旦添加了实时功能,ReceiverID就可以帮助我们过滤消息。

接下来,让我们编写一个HTTP处理程序。虽然有点长,但是没什么好怕的。

func createConversation(w http。ResponseWriter。request){ var input struct { Username string ` JSON:& # 34;用户名& # 34;`} defer r . body . close()if err:= JSON。NewDecoder(r.Body)。解码(& input);呃!= nil {http。错误(w,err。Error(),http。status badrequest)return }输入。用户名=字符串。TrimSpace(输入。用户名)如果输入。用户名= = & # 34;"{respond(w,Errors { map[string]string { & # 34;用户名& # 34;: "需要用户名& # 34;,}},http。StatusUnprocessableEntity)return } CTX:= r . Context()auth userid:= CTX。值(keyAuthUserID)。(字符串)tx,err := db。BeginTx(ctx,nil)if err!= nil {响应错误(w,fmt。errorf(& # 34;无法开始发送:% v & # 34,err))return}defer tx。roll back()var other participant user if err:= tx。QueryRowContext(ctx,` SELECT id,atar _ URL FROM users WHERE username = $ 1 `,input。用户名)。扫描(&otherParticipant。ID和其他参与者。atarURL,);err == sql。ErrNoRows {http。错误(w,& # 34;找不到用户& # 34;,http。StatusNotFound)如果err则返回} else!= nil {响应错误(w,fmt。errorf(& # 34;无法查询其他参与者:% v & # 34,err))return}otherParticipant。用户名=输入。用户名if otherParticipant。ID == authUserID {http。错误(w,& # 34;试着和别人开始对话& # 34;,http。status forbidden)return } var conversation id stringif err:= tx。QueryRowContext(ctx,` select conversation _ id FROM participants WHERE user _ id = $ 1 intersect select conversation _ id FROM participants WHERE user _ id = $ 2 `,authUserID,otherParticipant。ID)。扫描(& conversation id);呃!= nil && err!= sql。ErrNoRows {respondError(w,fmt。errorf(& # 34;无法查询公共对话id:% v & # 34;,err))return } else if err = = nil { http。重定向(w,r,& # 34;/API/conversations/& # 34;+conversationID,http。status found)return } var conversation conversation if err = tx。QueryRowContext(ctx,` insert INTO conversations DEFAULT values returning id `)。扫描(&对话。ID);呃!= nil {响应错误(w,fmt。errorf(& # 34;无法插入对话:% v & # 34,err))return}if _,err = tx。ExecContext(ctx,` INSERT INTO participants (user_id,conversation_id)值(,),(,)`,authUserID,conversation。ID,其他参与者。ID);呃!= nil {响应错误(w,fmt。errorf(& # 34;无法插入参与者:% v & # 34,err))如果err = tx,则返回}。commit();呃!= nil {响应错误(w,fmt。errorf(& # 34;无法提交tx以创建对话:% v & # 34,呃))return}对话。other participant = & other participant回复(w,对话,http。statuscreated)}在这个端点,你会向/api/conversations发送一个POST请求,请求的JSON体包含了想要通话的用户的用户名。

因此,首先需要将请求体解析成包含用户名的结构。然后,验证用户名不能是空。

type Errors struct { Errors map[string]string ` JSON:& # 34;错误& # 34;`}这是错误消息的结构错误,它只是一个映射。如果您输入用户名空,您将得到一个JSON,其中包含422不可处理的实体错误消息。

{"错误& # 34;: {"用户名& # 34;: "需要用户名& # 34;}}然后,我们开始执行SQL事务。我们只收到了用户名,但实际上,我们需要知道实际的用户ID。所以交易的第一个内容就是查询另一个参与者的ID和头像。如果找不到用户,我们将返回404 Not Found错误。此外,如果找到的用户恰好与“当前认证的用户”相同,我们应该返回一个403禁止错误。这是因为对话应该只在两个不同的用户之间发起,而不是在同一个人之间发起。

然后,我们试图找到两个用户共享的对话,因此我们需要使用INTERSECT语句。如果存在,只需通过/API/conversations/{ conversation id }重定向到对话并返回即可。

如果没有找到共享对话,我们需要创建一个新的对话并添加指定的两个参与者。最后,我们提交事务并用新创建的对话框进行响应。

获取对话列表

Endpoint /api/conversations将获取当前通过身份验证的用户的所有会话。

func getConversations(w http。ResponseWriter。request){ CTX:= r . Context()authUserID:= CTX。值(keyAuthUserID)。(字符串)行,err := db。QueryContext(ctx,’ SELECTconversations.id,auth _ user.messages _ read _ at & ltmessages.created_at AS has _ unread _ messages,messages.id,messages.content,messages . created _ at,messages.user_id = AS mine,other_users.id,other_users.username,other _ users . atar _ URL from conversations inner JOIN messages ON conversations . last _ message _ id = messages . id in ner JOIN participants ON other _ participants . conversation _ id = conversations . id and other _ participants . user _ id!= $ 1 inner JOIN users other _ users ON other _ participants . user _ id = other _ users . id inner JOIN participants auth _ user . conversation _ id = conversations . id and auth _ user . user _ id = $ 1 order BY messages . created _ at desc `,authUserID)if err!= nil {响应错误(w,fmt。errorf(& # 34;无法查询对话:% v & # 34,err))return }延期行。close()conversations:= make([]Conversation,0)for rows。next(){ var conversation conversation var last message message var other participant user if err = rows。扫描(&对话。ID和对话。HasUnreadMessages,&lastMessage。标识&lastMessage。内容&lastMessage。CreatedAt和lastMessage。我和其他参与者。ID和其他参与者。用户名和其他参与者。atarURL,);呃!= nil {响应错误(w,fmt。errorf(& # 34;无法扫描对话:% v & # 34,呃))return }对话。last message = & lastMessageconversation。other participant = & other participant conversations = append(conversations,conversation)}if err = rows。err();呃!= nil {响应错误(w,fmt。errorf(& # 34;无法迭代对话:% v & # 34,err)) return} respond (w,conversations,http。statusok)}这个处理程序只查询数据库。它通过一些连接查询会话表…首先,它从消息表中获取最后一条消息。然后,根据ID与当前认证用户不同的条件,从参与者表中找到会话中的另一个参与者。然后加入到用户表中,获取用户的用户名和头像。最后再次加入参与者表,在相反的条件下从表中找出另一个参与会话的用户,实际上就是当前认证的用户。我们将比较消息中的messages_read_at和created_at字段,以确定对话中是否有未读消息。然后,我们使用user_id字段来确定该消息是否属于“I”(指当前经过身份验证的用户)。

注意,这个查询过程假设只有两个用户参与对话,并且只适用于这种情况。另外,这种设计并不是很适合需要显示未读消息数量的情况。如果需要显示未读消息的数量,我认为可以在参与者表中添加一个unread_messages_count INT字段,每次创建新消息时递增,如果用户已读,则重置该字段。

接下来,您需要遍历每个记录,扫描每个现有的会话以创建一个会话片段,并在结束时进行响应。

找到单个对话

端点/API/conversations/{ conversation ID }将根据其ID响应单个会话。

func getConversation(w http。ResponseWriter。request){ CTX:= r . Context()authUserID:= CTX。值(keyAuthUserID)。(字符串)conversationID := way。Param(ctx,& # 34;conversationID & # 34)var conversation conversation var other participant user if err:= db。QueryRowContext(ctx,’ selectin null(auth _ user . messages _ read _ at & lt;messages.created_at,false) AS has_unread_messages,other_users.id,other_users.username,other _ users . atar _ URL from conversations left JOIN messages ON conversations . last _ message _ id = messages . id in ner JOIN participants other _ participants ON other _ participants . conversation _ id = conversations . id and other _ participants . user _ id!= $ 1内部加入用户other _ users ON other _ participants . user _ id = other _ users . id内部加入参与者auth _ user ON auth _ user . conversation _ id = conversations . id和auth _ user . user _ id = $ 1 where conversations . id = $ 2 `,authUserID,conversationID)。扫描(&对话。HasUnreadMessages,&otherParticipant。ID和其他参与者。用户名和其他参与者。atarURL,);err == sql。ErrNoRows {http。错误(w,& # 34;找不到对话& # 34;,http。StatusNotFound)如果err则返回} else!= nil {响应错误(w,fmt。errorf(& # 34;无法查询对话:% v & # 34,呃))return}对话。ID = ConversationIdConversation。其他参与者= &其他参与者响应(w,对话,http。statusok)}这里的查询与之前有些类似。虽然我们并不关心最后一条消息的显示,所以忽略了与之相关的一些字段,但是我们需要根据这条消息来判断对话中是否有未读消息。此时我们用左连接代替内连接,因为last_message_id字段是可空的(可以是空);在其他情况下,我们无法获得任何记录。出于同样的原因,我们在has_unread_messages的比较中使用IFNULL语句。最后,我们按ID过滤。

如果查询没有返回任何记录,我们的响应将返回404 Not Found错误,否则响应将返回200 OK和Found会话。

这篇文章以创建一些对话端点结束。

在下一篇文章中,我们将看到如何创建和列出消息。

源代码

via:https://nicolasparada . netlify . com/posts/go-messenger-conversations/

作者:尼古拉斯·帕拉达题目:lujun9972译者:PsiACE校对:wxy

本文由LCTT原创,并由Linux中国提供荣誉。

点击“了解更多”可访问文内链接

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。

发表回复

登录后才能评论