Files
nl-game-api/internal/handler/friend.go
2026-08-14 13:17:03 +08:00

276 lines
8.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"strconv"
"strings"
"github.com/gin-gonic/gin"
"nl-game-api-gin/internal/database"
"nl-game-api-gin/internal/middleware"
"nl-game-api-gin/internal/model"
"nl-game-api-gin/internal/room"
"nl-game-api-gin/pkg/resp"
)
// userBrief 用户摘要(好友列表/会话列表/搜索结果通用)
type userBrief struct {
UserID int `json:"user_id"` // 用户ID
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"` // 昵称
Avatar string `json:"avatar"` // emoji 头像
Online bool `json:"online"` // 是否在线(有活跃 WebSocket 连接)
}
// briefOf 把用户模型转为摘要
func briefOf(u *model.User, online bool) userBrief {
return userBrief{UserID: u.ID, Username: u.Username, Nickname: u.Nickname, Avatar: u.Avatar, Online: online}
}
// loadBriefs 批量加载用户摘要(自动附带在线状态)
func loadBriefs(ids []int) map[int]userBrief {
out := map[int]userBrief{}
if len(ids) == 0 {
return out
}
var users []model.User
database.DB.Where("id IN ?", ids).Find(&users)
online := room.OnlineMap(ids)
for i := range users {
u := &users[i]
out[u.ID] = briefOf(u, online[u.ID])
}
return out
}
// isFriend 两人是否已互为好友(任一方向的已通过记录)
func isFriend(a, b int) bool {
var cnt int64
database.DB.Model(&model.Friend{}).
Where("status = ? AND ((user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?))",
model.FriendStatusAccepted, a, b, b, a).
Count(&cnt)
return cnt > 0
}
// FriendList 我的好友列表(在线的排在前面)
func FriendList(c *gin.Context) {
uid := middleware.UID(c)
var rels []model.Friend
database.DB.Where("status = ? AND (user_id = ? OR friend_id = ?)", model.FriendStatusAccepted, uid, uid).
Order("updated_at DESC").Find(&rels)
ids := make([]int, 0, len(rels))
sinceMap := map[int]int64{}
for _, r := range rels {
peer := r.UserID
if peer == uid {
peer = r.FriendID
}
ids = append(ids, peer)
sinceMap[peer] = r.UpdatedAt
}
briefs := loadBriefs(ids)
type friendItem struct {
userBrief
Since int64 `json:"since"` // 成为好友时间
}
list := make([]friendItem, 0, len(ids))
for _, id := range ids {
if b, ok := briefs[id]; ok {
list = append(list, friendItem{userBrief: b, Since: sinceMap[id]})
}
}
// 在线的排前面(保持各自原有相对顺序)
sorted := make([]friendItem, 0, len(list))
for _, it := range list {
if it.Online {
sorted = append(sorted, it)
}
}
for _, it := range list {
if !it.Online {
sorted = append(sorted, it)
}
}
resp.OK(c, sorted)
}
// FriendRequests 好友申请received=发给我的待处理申请sent=我发出的待验证申请
func FriendRequests(c *gin.Context) {
uid := middleware.UID(c)
var in, out []model.Friend
database.DB.Where("friend_id = ? AND status = ?", uid, model.FriendStatusPending).Order("id DESC").Find(&in)
database.DB.Where("user_id = ? AND status = ?", uid, model.FriendStatusPending).Order("id DESC").Find(&out)
ids := make([]int, 0, len(in)+len(out))
for _, r := range in {
ids = append(ids, r.UserID)
}
for _, r := range out {
ids = append(ids, r.FriendID)
}
briefs := loadBriefs(ids)
type reqItem struct {
ID int `json:"id"` // 关系ID同意/拒绝时用)
User userBrief `json:"user"` // 对方信息
At int64 `json:"at"` // 申请时间
}
received := make([]reqItem, 0, len(in))
for _, r := range in {
received = append(received, reqItem{ID: r.ID, User: briefs[r.UserID], At: r.CreatedAt})
}
sent := make([]reqItem, 0, len(out))
for _, r := range out {
sent = append(sent, reqItem{ID: r.ID, User: briefs[r.FriendID], At: r.CreatedAt})
}
resp.OK(c, gin.H{"received": received, "sent": sent})
}
// FriendSearch 按用户名/昵称搜索用户(加好友入口),附带与我的关系状态
func FriendSearch(c *gin.Context) {
uid := middleware.UID(c)
kw := strings.TrimSpace(c.Query("keyword"))
if kw == "" {
resp.OK(c, []any{})
return
}
like := "%" + kw + "%"
var users []model.User
database.DB.Where("id != ? AND status = ? AND (username LIKE ? OR nickname LIKE ?)",
uid, model.UserStatusNormal, like, like).Limit(10).Find(&users)
ids := make([]int, 0, len(users))
for _, u := range users {
ids = append(ids, u.ID)
}
online := room.OnlineMap(ids)
// 一次取出我与这批用户的全部关系,标注状态
var rels []model.Friend
if len(ids) > 0 {
database.DB.Where("(user_id = ? AND friend_id IN ?) OR (friend_id = ? AND user_id IN ?)", uid, ids, uid, ids).Find(&rels)
}
// relation: none=无关系 friend=已是好友 pending_out=我已申请 pending_in=对方已申请我
relMap := map[int]string{}
for _, r := range rels {
peer := r.UserID
dir := "pending_in"
if r.UserID == uid {
peer = r.FriendID
dir = "pending_out"
}
if r.Status == model.FriendStatusAccepted {
relMap[peer] = "friend"
} else if relMap[peer] != "friend" {
relMap[peer] = dir
}
}
type searchItem struct {
userBrief
Relation string `json:"relation"` // none/friend/pending_out/pending_in
}
list := make([]searchItem, 0, len(users))
for i := range users {
u := &users[i]
rel := relMap[u.ID]
if rel == "" {
rel = "none"
}
list = append(list, searchItem{userBrief: briefOf(u, online[u.ID]), Relation: rel})
}
resp.OK(c, list)
}
// friendReqBody 发起好友申请请求体
type friendReqBody struct {
UserID int `json:"user_id" binding:"required"` // 要添加的用户ID
}
// FriendRequest 发起好友申请:对方在线时实时推送;若对方已申请过我则直接成为好友
func FriendRequest(c *gin.Context) {
uid := middleware.UID(c)
var req friendReqBody
if err := c.ShouldBindJSON(&req); err != nil || req.UserID == uid {
resp.Fail(c, "参数有误")
return
}
var target model.User
if err := database.DB.First(&target, req.UserID).Error; err != nil || target.Status != model.UserStatusNormal {
resp.Fail(c, "用户不存在")
return
}
var me model.User
database.DB.First(&me, uid)
// 已有关系判断(两个方向)
var exist model.Friend
if err := database.DB.Where("user_id = ? AND friend_id = ?", uid, req.UserID).First(&exist).Error; err == nil {
if exist.Status == model.FriendStatusAccepted {
resp.Fail(c, "你们已经是好友了")
} else {
resp.Fail(c, "已发送过申请,等待对方同意")
}
return
}
var reverse model.Friend
if err := database.DB.Where("user_id = ? AND friend_id = ?", req.UserID, uid).First(&reverse).Error; err == nil {
if reverse.Status == model.FriendStatusAccepted {
resp.Fail(c, "你们已经是好友了")
return
}
// 对方已申请过我:视为互相想加,直接通过
database.DB.Model(&reverse).Update("status", model.FriendStatusAccepted)
room.PushToUser(req.UserID, "friend_event", gin.H{"kind": "accept", "user": briefOf(&me, true)})
resp.OK(c, gin.H{"relation": "friend", "msg": "对方也申请过你,已直接成为好友"})
return
}
rel := model.Friend{UserID: uid, FriendID: req.UserID, Status: model.FriendStatusPending}
if err := database.DB.Create(&rel).Error; err != nil {
resp.Fail(c, "申请失败,请稍后重试")
return
}
// 对方在线则实时提醒
room.PushToUser(req.UserID, "friend_event", gin.H{"kind": "request", "user": briefOf(&me, true)})
resp.OK(c, gin.H{"relation": "pending_out"})
}
// friendRespondBody 处理好友申请请求体
type friendRespondBody struct {
ID int `json:"id" binding:"required"` // 关系ID来自申请列表
Accept bool `json:"accept"` // true=同意 false=拒绝
}
// FriendRespond 同意/拒绝好友申请(同意后通知申请人)
func FriendRespond(c *gin.Context) {
uid := middleware.UID(c)
var req friendRespondBody
if err := c.ShouldBindJSON(&req); err != nil {
resp.Fail(c, "参数有误")
return
}
var rel model.Friend
if err := database.DB.First(&rel, req.ID).Error; err != nil || rel.FriendID != uid || rel.Status != model.FriendStatusPending {
resp.Fail(c, "申请不存在或已处理")
return
}
if !req.Accept {
database.DB.Delete(&rel)
resp.OK(c, gin.H{"relation": "none"})
return
}
database.DB.Model(&rel).Update("status", model.FriendStatusAccepted)
var me model.User
database.DB.First(&me, uid)
room.PushToUser(rel.UserID, "friend_event", gin.H{"kind": "accept", "user": briefOf(&me, true)})
resp.OK(c, gin.H{"relation": "friend"})
}
// FriendDelete 删除好友(清除双方全部关系记录,聊天记录保留)
func FriendDelete(c *gin.Context) {
uid := middleware.UID(c)
peer, _ := strconv.Atoi(c.Param("uid"))
if peer <= 0 {
resp.Fail(c, "参数有误")
return
}
database.DB.Where("(user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)", uid, peer, peer, uid).
Delete(&model.Friend{})
resp.OK(c, nil)
}