Step 07 — 08 | 從公開聊天室進化成 1 對 1 私訊
// 前端送出
ws.send(JSON.stringify({
type: "pm",
to: 1, // 對方的 userID
text: "你好"
}));
// 在線名單
[System] Users: 1:Alice,2:Bob,3:Carol
// 私訊
[PM:Alice:14:30:05] 你好
為什麼不全用 JSON?在線名單和系統訊息用字串格式更簡單。私訊的發送端用 JSON 是因為需要帶型別(userID 是 uint),字串解析會很醜。
type Hub struct {
Clients map[*Client]bool // 遍歷所有人(廣播用)
ClientsByUserID map[uint]*Client // O(1) 查找(私訊用)
}
| 操作 | 用哪個 map | 複雜度 |
|---|---|---|
| 廣播在線名單 | Clients(遍歷) | O(n) |
| 私訊指定用戶 | ClientsByUserID(查找) | O(1) |
| Register | 兩個都加 | O(1) |
| Unregister | 兩個都刪 | O(1) |
func (h *Hub) SendToUser(userID uint, message []byte) {
h.mu.Lock()
defer h.mu.Unlock()
if client, ok := h.ClientsByUserID[userID]; ok {
select {
case client.Send <- message:
default: // client 卡住了,跳過
}
}
}
type Message struct {
ID uint
SenderID uint // 誰送的
ReceiverID uint // 送給誰
Content string // 訊息內容
CreatedAt time.Time // 自動填入
}
DB.Where(
"(sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?)",
userID, peerID, peerID, userID,
).Order("created_at ASC").Limit(100).Find(&messages)
A 發給 B 的 + B 發給 A 的,按時間排序,最多 100 筆。
歷史訊息只在第一次開聊天時拉。之後的新訊息由 WebSocket 即時推送,不重複查 DB。
// JWT MapClaims 把所有數字解析成 float64(JSON 規格)
UserID: uint(c.GetFloat64("user_id"))
JSON 沒有整數型別,所有數字都是浮點數。所以從 JWT claims 取出後要轉型。