自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

構(gòu)建一個即時消息應(yīng)用(三):對話

開發(fā) 后端
在這篇帖子中,我們將會編寫一些端點endpoint來完成像“創(chuàng)建對話”、“獲取對話列表”以及“找到單個對話”這樣的任務(wù)。

[[320477]]

本文是該系列的第三篇。

在我們的即時消息應(yīng)用中,消息表現(xiàn)為兩個參與者對話的堆疊。如果你想要開始一場對話,就應(yīng)該向應(yīng)用提供你想要交談的用戶,而當對話創(chuàng)建后(如果該對話此前并不存在),就可以向該對話發(fā)送消息。

就前端而言,我們可能想要顯示一份近期對話列表。并在此處顯示對話的最后一條消息以及另一個參與者的姓名和頭像。

在這篇帖子中,我們將會編寫一些端點endpoint來完成像“創(chuàng)建對話”、“獲取對話列表”以及“找到單個對話”這樣的任務(wù)。

首先,要在主函數(shù) main() 中添加下面的路由。

  1. router.HandleFunc("POST", "/api/conversations", requireJSON(guard(createConversation)))
  2. router.HandleFunc("GET", "/api/conversations", guard(getConversations))
  3. router.HandleFunc("GET", "/api/conversations/:conversationID", guard(getConversation))

這三個端點都需要進行身份驗證,所以我們將會使用 guard() 中間件。我們也會構(gòu)建一個新的中間件,用于檢查請求內(nèi)容是否為 JSON 格式。

JSON 請求檢查中間件

  1. func requireJSON(handler http.HandlerFunc) http.HandlerFunc {
  2. return func(w http.ResponseWriter, r *http.Request) {
  3. if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
  4. http.Error(w, "Content type of application/json required", http.StatusUnsupportedMediaType)
  5. return
  6. }
  7. handler(w, r)
  8. }
  9. }

如果請求request不是 JSON 格式,那么它會返回 415 Unsupported Media Type(不支持的媒體類型)錯誤。

創(chuàng)建對話

  1. type Conversation struct {
  2. ID string `json:"id"`
  3. OtherParticipant *User `json:"otherParticipant"`
  4. LastMessage *Message `json:"lastMessage"`
  5. HasUnreadMessages bool `json:"hasUnreadMessages"`
  6. }

就像上面的代碼那樣,對話中保持對另一個參與者和最后一條消息的引用,還有一個 bool 類型的字段,用來告知是否有未讀消息。

  1. type Message struct {
  2. ID string `json:"id"`
  3. Content string `json:"content"`
  4. UserID string `json:"-"`
  5. ConversationID string `json:"conversationID,omitempty"`
  6. CreatedAt time.Time `json:"createdAt"`
  7. Mine bool `json:"mine"`
  8. ReceiverID string `json:"-"`
  9. }

我們會在下一篇文章介紹與消息相關(guān)的內(nèi)容,但由于我們這里也需要用到它,所以先定義了 Message 結(jié)構(gòu)體。其中大多數(shù)字段與數(shù)據(jù)庫表一致。我們需要使用 Mine 來斷定消息是否屬于當前已驗證用戶所有。一旦加入實時功能,ReceiverID 可以幫助我們過濾消息。

接下來讓我們編寫 HTTP 處理程序。盡管它有些長,但也沒什么好怕的。

  1. func createConversation(w http.ResponseWriter, r *http.Request) {
  2. var input struct {
  3. Username string `json:"username"`
  4. }
  5. defer r.Body.Close()
  6. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  7. http.Error(w, err.Error(), http.StatusBadRequest)
  8. return
  9. }
  10.  
  11. input.Username = strings.TrimSpace(input.Username)
  12. if input.Username == "" {
  13. respond(w, Errors{map[string]string{
  14. "username": "Username required",
  15. }}, http.StatusUnprocessableEntity)
  16. return
  17. }
  18.  
  19. ctx := r.Context()
  20. authUserID := ctx.Value(keyAuthUserID).(string)
  21.  
  22. tx, err := db.BeginTx(ctx, nil)
  23. if err != nil {
  24. respondError(w, fmt.Errorf("could not begin tx: %v", err))
  25. return
  26. }
  27. defer tx.Rollback()
  28.  
  29. var otherParticipant User
  30. if err := tx.QueryRowContext(ctx, `
  31. SELECT id, avatar_url FROM users WHERE username = $1
  32. `, input.Username).Scan(
  33. &otherParticipant.ID,
  34. &otherParticipant.AvatarURL,
  35. ); err == sql.ErrNoRows {
  36. http.Error(w, "User not found", http.StatusNotFound)
  37. return
  38. } else if err != nil {
  39. respondError(w, fmt.Errorf("could not query other participant: %v", err))
  40. return
  41. }
  42.  
  43. otherParticipant.Username = input.Username
  44.  
  45. if otherParticipant.ID == authUserID {
  46. http.Error(w, "Try start a conversation with someone else", http.StatusForbidden)
  47. return
  48. }
  49.  
  50. var conversationID string
  51. if err := tx.QueryRowContext(ctx, `
  52. SELECT conversation_id FROM participants WHERE user_id = $1
  53. INTERSECT
  54. SELECT conversation_id FROM participants WHERE user_id = $2
  55. `, authUserID, otherParticipant.ID).Scan(&conversationID); err != nil && err != sql.ErrNoRows {
  56. respondError(w, fmt.Errorf("could not query common conversation id: %v", err))
  57. return
  58. } else if err == nil {
  59. http.Redirect(w, r, "/api/conversations/"+conversationID, http.StatusFound)
  60. return
  61. }
  62.  
  63. var conversation Conversation
  64. if err = tx.QueryRowContext(ctx, `
  65. INSERT INTO conversations DEFAULT VALUES
  66. RETURNING id
  67. `).Scan(&conversation.ID); err != nil {
  68. respondError(w, fmt.Errorf("could not insert conversation: %v", err))
  69. return
  70. }
  71.  
  72. if _, err = tx.ExecContext(ctx, `
  73. INSERT INTO participants (user_id, conversation_id) VALUES
  74. ($1, $2),
  75. ($3, $2)
  76. `, authUserID, conversation.ID, otherParticipant.ID); err != nil {
  77. respondError(w, fmt.Errorf("could not insert participants: %v", err))
  78. return
  79. }
  80.  
  81. if err = tx.Commit(); err != nil {
  82. respondError(w, fmt.Errorf("could not commit tx to create conversation: %v", err))
  83. return
  84. }
  85.  
  86. conversation.OtherParticipant = &otherParticipant
  87.  
  88. respond(w, conversation, http.StatusCreated)
  89. }

在此端點,你會向 /api/conversations 發(fā)送 POST 請求,請求的 JSON 主體中包含要對話的用戶的用戶名。

因此,首先需要將請求主體解析成包含用戶名的結(jié)構(gòu)。然后,校驗用戶名不能為空。

  1. type Errors struct {
  2. Errors map[string]string `json:"errors"`
  3. }

這是錯誤消息的結(jié)構(gòu)體 Errors,它僅僅是一個映射。如果輸入空用戶名,你就會得到一段帶有 422 Unprocessable Entity(無法處理的實體)錯誤消息的 JSON 。

  1. {
  2. "errors": {
  3. "username": "Username required"
  4. }
  5. }

然后,我們開始執(zhí)行 SQL 事務(wù)。收到的僅僅是用戶名,但事實上,我們需要知道實際的用戶 ID 。因此,事務(wù)的第一項內(nèi)容是查詢另一個參與者的 ID 和頭像。如果找不到該用戶,我們將會返回 404 Not Found(未找到) 錯誤。另外,如果找到的用戶恰好和“當前已驗證用戶”相同,我們應(yīng)該返回 403 Forbidden(拒絕處理)錯誤。這是由于對話只應(yīng)當在兩個不同的用戶之間發(fā)起,而不能是同一個。

然后,我們試圖找到這兩個用戶所共有的對話,所以需要使用 INTERSECT 語句。如果存在,只需要通過 /api/conversations/{conversationID} 重定向到該對話并將其返回。

如果未找到共有的對話,我們需要創(chuàng)建一個新的對話并添加指定的兩個參與者。最后,我們 COMMIT 該事務(wù)并使用新創(chuàng)建的對話進行響應(yīng)。

獲取對話列表

端點 /api/conversations 將獲取當前已驗證用戶的所有對話。

  1. func getConversations(w http.ResponseWriter, r *http.Request) {
  2. ctx := r.Context()
  3. authUserID := ctx.Value(keyAuthUserID).(string)
  4.  
  5. rows, err := db.QueryContext(ctx, `
  6. SELECT
  7. conversations.id,
  8. auth_user.messages_read_at < messages.created_at AS has_unread_messages,
  9. messages.id,
  10. messages.content,
  11. messages.created_at,
  12. messages.user_id = $1 AS mine,
  13. other_users.id,
  14. other_users.username,
  15. other_users.avatar_url
  16. FROM conversations
  17. INNER JOIN messages ON conversations.last_message_id = messages.id
  18. INNER JOIN participants other_participants
  19. ON other_participants.conversation_id = conversations.id
  20. AND other_participants.user_id != $1
  21. INNER JOIN users other_users ON other_participants.user_id = other_users.id
  22. INNER JOIN participants auth_user
  23. ON auth_user.conversation_id = conversations.id
  24. AND auth_user.user_id = $1
  25. ORDER BY messages.created_at DESC
  26. `, authUserID)
  27. if err != nil {
  28. respondError(w, fmt.Errorf("could not query conversations: %v", err))
  29. return
  30. }
  31. defer rows.Close()
  32.  
  33. conversations := make([]Conversation, 0)
  34. for rows.Next() {
  35. var conversation Conversation
  36. var lastMessage Message
  37. var otherParticipant User
  38. if err = rows.Scan(
  39. &conversation.ID,
  40. &conversation.HasUnreadMessages,
  41. &lastMessage.ID,
  42. &lastMessage.Content,
  43. &lastMessage.CreatedAt,
  44. &lastMessage.Mine,
  45. &otherParticipant.ID,
  46. &otherParticipant.Username,
  47. &otherParticipant.AvatarURL,
  48. ); err != nil {
  49. respondError(w, fmt.Errorf("could not scan conversation: %v", err))
  50. return
  51. }
  52.  
  53. conversation.LastMessage = &lastMessage
  54. conversation.OtherParticipant = &otherParticipant
  55. conversations = append(conversations, conversation)
  56. }
  57.  
  58. if err = rows.Err(); err != nil {
  59. respondError(w, fmt.Errorf("could not iterate over conversations: %v", err))
  60. return
  61. }
  62.  
  63. respond(w, conversations, http.StatusOK)
  64. }

該處理程序僅對數(shù)據(jù)庫進行查詢。它通過一些聯(lián)接來查詢對話表……首先,從消息表中獲取最后一條消息。然后依據(jù)“ID 與當前已驗證用戶不同”的條件,從參與者表找到對話的另一個參與者。然后聯(lián)接到用戶表以獲取該用戶的用戶名和頭像。最后,再次聯(lián)接參與者表,并以相反的條件從該表中找出參與對話的另一個用戶,其實就是當前已驗證用戶。我們會對比消息中的 messages_read_atcreated_at 兩個字段,以確定對話中是否存在未讀消息。然后,我們通過 user_id 字段來判定該消息是否屬于“我”(指當前已驗證用戶)。

注意,此查詢過程假定對話中只有兩個用戶參與,它也僅僅適用于這種情況。另外,該設(shè)計也不很適用于需要顯示未讀消息數(shù)量的情況。如果需要顯示未讀消息的數(shù)量,我認為可以在 participants 表上添加一個unread_messages_count INT 字段,并在每次創(chuàng)建新消息的時候遞增它,如果用戶已讀則重置該字段。

接下來需要遍歷每一條記錄,通過掃描每一個存在的對話來建立一個對話切片slice of conversations并在最后進行響應(yīng)。

找到單個對話

端點 /api/conversations/{conversationID} 會根據(jù) ID 對單個對話進行響應(yīng)。

  1. func getConversation(w http.ResponseWriter, r *http.Request) {
  2. ctx := r.Context()
  3. authUserID := ctx.Value(keyAuthUserID).(string)
  4. conversationID := way.Param(ctx, "conversationID")
  5.  
  6. var conversation Conversation
  7. var otherParticipant User
  8. if err := db.QueryRowContext(ctx, `
  9. SELECT
  10. IFNULL(auth_user.messages_read_at < messages.created_at, false) AS has_unread_messages,
  11. other_users.id,
  12. other_users.username,
  13. other_users.avatar_url
  14. FROM conversations
  15. LEFT JOIN messages ON conversations.last_message_id = messages.id
  16. INNER JOIN participants other_participants
  17. ON other_participants.conversation_id = conversations.id
  18. AND other_participants.user_id != $1
  19. INNER JOIN users other_users ON other_participants.user_id = other_users.id
  20. INNER JOIN participants auth_user
  21. ON auth_user.conversation_id = conversations.id
  22. AND auth_user.user_id = $1
  23. WHERE conversations.id = $2
  24. `, authUserID, conversationID).Scan(
  25. &conversation.HasUnreadMessages,
  26. &otherParticipant.ID,
  27. &otherParticipant.Username,
  28. &otherParticipant.AvatarURL,
  29. ); err == sql.ErrNoRows {
  30. http.Error(w, "Conversation not found", http.StatusNotFound)
  31. return
  32. } else if err != nil {
  33. respondError(w, fmt.Errorf("could not query conversation: %v", err))
  34. return
  35. }
  36.  
  37. conversation.ID = conversationID
  38. conversation.OtherParticipant = &otherParticipant
  39.  
  40. respond(w, conversation, http.StatusOK)
  41. }

這里的查詢與之前有點類似。盡管我們并不關(guān)心最后一條消息的顯示問題,并因此忽略了與之相關(guān)的一些字段,但是我們需要根據(jù)這條消息來判斷對話中是否存在未讀消息。此時,我們使用 LEFT JOIN 來代替 INNER JOIN,因為 last_message_id 字段是 NULLABLE(可以為空)的;而其他情況下,我們無法得到任何記錄。基于同樣的理由,我們在 has_unread_messages 的比較中使用了 IFNULL 語句。最后,我們按 ID 進行過濾。

如果查詢沒有返回任何記錄,我們的響應(yīng)會返回 404 Not Found 錯誤,否則響應(yīng)將會返回 200 OK 以及找到的對話。


本篇帖子以創(chuàng)建了一些對話端點結(jié)束。

在下一篇帖子中,我們將會看到如何創(chuàng)建并列出消息。

 

 

責任編輯:龐桂玉 來源: Linux中國
相關(guān)推薦

2020-10-09 12:45:19

創(chuàng)建消息即時消息編程語言

2019-09-29 15:25:13

CockroachDBGoJavaScript

2020-10-09 15:00:56

實時消息編程語言

2019-10-28 20:12:40

OAuthGuard中間件編程語言

2020-10-12 09:20:13

即時消息Access頁面編程語言

2020-10-19 16:20:38

即時消息Conversatio編程語言

2020-10-16 14:40:20

即時消息Home頁面編程語言

2020-10-10 20:51:10

即時消息編程語言

2021-03-25 08:29:33

SpringBootWebSocket即時消息

2023-08-14 08:01:12

websocket8g用戶

2015-03-18 15:37:19

社交APP場景

2014-10-15 11:01:02

Web應(yīng)用測試應(yīng)用

2018-08-22 17:32:45

2022-02-10 07:03:32

流量應(yīng)用架構(gòu)數(shù)據(jù)交換

2021-07-14 17:39:46

ReactRails API前端組件

2023-09-21 08:00:00

ChatGPT編程工具

2011-12-27 09:49:50

AndroidiOSFlurry

2021-12-03 00:02:01

通訊工具即時

2023-09-15 10:10:05

R 語言

2010-05-24 09:51:37

System Cent
點贊
收藏

51CTO技術(shù)棧公眾號