209 lines
5.8 KiB
Go
209 lines
5.8 KiB
Go
/**
|
||
* 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}, "已标记已读")
|
||
}
|