Files
nl-im-service/internal/api/settings_handler.go
2026-08-24 15:29:53 +08:00

209 lines
5.8 KiB
Go
Raw 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 api
* 作用:用户设置与消息已读回执 HTTP 接口
*/
package api
import (
"encoding/json"
"time"
"xk-websocket-v2/internal/model"
"xk-websocket-v2/internal/service"
"xk-websocket-v2/internal/utils"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
/**
* GetUserSettingsHandler
* 功能:获取当前用户的全部设置
*/
func GetUserSettingsHandler(c *gin.Context) {
userID, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
var rows []model.UserSetting
if err := service.ChatSvc.DB.Where("user_id = ?", userID.(string)).Find(&rows).Error; err != nil {
utils.InternalError(c, "查询失败")
return
}
result := make(map[string]string, len(rows))
for _, row := range rows {
result[row.SettingKey] = row.SettingValue
}
utils.SuccessWithData(c, result, "获取成功")
}
/**
* UpdateUserSettingsHandler
* 功能:批量更新用户设置
*/
func UpdateUserSettingsHandler(c *gin.Context) {
userID, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
var req struct {
Settings map[string]string `json:"settings" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误")
return
}
// 上限校验:设置项数量与 key/value 长度设置合理上限,
// 防止单次提交超大 map 触发大量写库(滥用/误用防护)
const (
maxSettingsPerRequest = 100
maxKeyLen = 100
maxValueLen = 4000
)
if len(req.Settings) > maxSettingsPerRequest {
utils.BadRequest(c, "设置项过多")
return
}
for key, val := range req.Settings {
if key == "" || len(key) > maxKeyLen {
utils.BadRequest(c, "设置键不合法")
return
}
if len(val) > maxValueLen {
utils.BadRequest(c, "设置值过长")
return
}
}
uid := userID.(string)
now := time.Now()
// 事务包裹批量写入:原实现循环逐条 Save中途某条失败时前面的 key 已落库、
// 接口却返回错误,用户设置停留在"新旧混合"的中间状态;
// 改为同一事务内全部成功才提交,任一失败整体回滚,保证批量更新的原子性
if err := service.ChatSvc.DB.Transaction(func(tx *gorm.DB) error {
for key, val := range req.Settings {
setting := model.UserSetting{
UserID: uid,
SettingKey: key,
SettingValue: val,
UpdatedAt: now,
}
if err := tx.Save(&setting).Error; err != nil {
return err
}
}
return nil
}); err != nil {
utils.InternalError(c, "保存设置失败")
return
}
GetUserSettingsHandler(c)
}
/**
* MarkMessagesReadHandler
* 功能:批量标记消息已读
*/
func MarkMessagesReadHandler(c *gin.Context) {
userID, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
var req struct {
MessageIDs []uint `json:"message_ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误")
return
}
uid := userID.(string)
now := time.Now()
// 批量查询消息:一条 IN 查询取回,
// 原实现每条消息 FirstOrCreate + First 各一次N+1一次标记50条已读要打100+次库
var msgs []model.ChatMessage
if err := service.ChatSvc.DB.Where("id IN ?", req.MessageIDs).Find(&msgs).Error; err != nil {
utils.InternalError(c, "db error")
return
}
// 越权校验:只允许标记"自己所在房间"的消息为已读(口径与 HistoryHandler 的
// IsRoomMember 鉴权对称。消息ID由客户端任意提交不校验成员身份的话
// 任何登录用户都能对别人房间的消息伪造已读回执,发送者会收到虚假的 messages_read 推送。
// 同批消息通常集中在少数房间,按房间去重后每个房间只查一次成员关系,避免逐条 N 次查库;
// 非成员房间的消息静默剔除批量接口部分生效语义readIDs 只返回真正标记成功的部分。
roomAllowed := make(map[string]bool)
visibleMsgs := msgs[:0]
for _, msg := range msgs {
allowed, checked := roomAllowed[msg.RoomID]
if !checked {
allowed = service.RoomSvc.IsRoomMember(msg.RoomID, uid)
roomAllowed[msg.RoomID] = allowed
}
if allowed {
visibleMsgs = append(visibleMsgs, msg)
}
}
msgs = visibleMsgs
// 批量插入已读回执:靠 (message_id, user_id) 唯一索引 + OnConflict DoNothing
// 实现幂等,重复标记已读不报错也不更新(保留首次已读时间)
receipts := make([]model.MessageReadReceipt, 0, len(msgs))
readIDs := make([]uint, 0, len(msgs))
for _, msg := range msgs {
receipts = append(receipts, model.MessageReadReceipt{
MessageID: msg.ID,
UserID: uid,
ReadAt: now,
})
readIDs = append(readIDs, msg.ID)
}
if len(receipts) > 0 {
if err := service.ChatSvc.DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&receipts).Error; err != nil {
utils.InternalError(c, "标记已读失败")
return
}
}
// 按"发送者+房间"分组推送已读回执:同一发送者在同一房间的多条消息合并成一条推送,
// 原实现每条消息推一次,批量已读时会向发送者刷屏式推送
type senderRoomKey struct {
sender string
room string
}
grouped := make(map[senderRoomKey][]uint)
for _, msg := range msgs {
if msg.SenderUserID == "" || msg.SenderUserID == uid {
continue
}
key := senderRoomKey{sender: msg.SenderUserID, room: msg.RoomID}
grouped[key] = append(grouped[key], msg.ID)
}
for key, ids := range grouped {
payload := model.WsPayload{
RequestType: "messages_read",
Data: map[string]interface{}{
"message_ids": ids,
"reader_id": uid,
"room_id": key.room,
},
}
if bytes, err := json.Marshal(payload); err == nil {
service.ChatSvc.DispatchMessage(key.sender, bytes)
}
}
utils.SuccessWithData(c, gin.H{"message_ids": readIDs}, "已标记已读")
}