Files

276 lines
8.7 KiB
Go
Raw Permalink Normal View History

2026-08-14 13:17:03 +08:00
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)
}