安全与可靠性: 修复配额并发、投票事务、密码授权与导出完整性
This commit is contained in:
@@ -235,6 +235,9 @@ func SendMessage(ctx context.Context, cache cache.Cache) {
|
||||
todayCount, err := db.Users.CountTodayMessagesFromUser(ctx.Request().Context(), pageUser.ID, ctx.User.ID)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to count today messages")
|
||||
ctx.SetInternalErrorFlash()
|
||||
ctx.Redirect(fmt.Sprintf("/_/%s/chat/%s", domain, token))
|
||||
return
|
||||
}
|
||||
if todayCount >= int64(pageUser.DailyMessageLimit) {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "chat.errDailyLimit", pageUser.DailyMessageLimit))
|
||||
@@ -330,54 +333,27 @@ func SendMessage(ctx context.Context, cache cache.Cache) {
|
||||
}
|
||||
}
|
||||
|
||||
// 原子地递增消息计数:真正的上限检查点(先于消息落库,防止超额消息写入)。
|
||||
// 并发场景(对话双方同时发送)下,仅当 message_count < max 时才 +1 成功。
|
||||
ok, err := db.Conversations.IncrementMessageCountIfBelow(ctx.Request().Context(), conv.ID, conv.MaxMessages)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to increment message count")
|
||||
ctx.SetInternalErrorFlash()
|
||||
ctx.Redirect(fmt.Sprintf("/_/%s/chat/%s", domain, token))
|
||||
return
|
||||
var image *db.CreateMessageImageOptions
|
||||
if imageURL != "" {
|
||||
image = &db.CreateMessageImageOptions{URL: imageURL, ObjectKey: imageKey, FileName: imageHeader.Filename, FileSize: imageHeader.Size}
|
||||
}
|
||||
if !ok {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "chat.errLimitReached"))
|
||||
ctx.Redirect(fmt.Sprintf("/_/%s/chat/%s", domain, token))
|
||||
return
|
||||
}
|
||||
|
||||
// 创建消息
|
||||
msg, err := db.Messages.Create(ctx.Request().Context(), db.CreateMessageOptions{
|
||||
ConversationID: conv.ID,
|
||||
SenderID: ctx.User.ID,
|
||||
Content: content,
|
||||
_, err = db.Conversations.SendMessage(ctx.Request().Context(), db.SendConversationMessageOptions{
|
||||
ConversationID: conv.ID, SenderID: ctx.User.ID, Content: content, Image: image,
|
||||
})
|
||||
if err != nil {
|
||||
// 计数已递增但消息落库失败:回滚计数,避免占用配额
|
||||
if rerr := db.Conversations.DecrementMessageCount(ctx.Request().Context(), conv.ID); rerr != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(rerr).Error("Failed to rollback message count")
|
||||
switch {
|
||||
case errors.Is(err, db.ErrDailyMessageLimit):
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "chat.errDailyLimit", pageUser.DailyMessageLimit))
|
||||
case errors.Is(err, db.ErrConversationLimit):
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "chat.errLimitReached"))
|
||||
default:
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to send message")
|
||||
ctx.SetInternalErrorFlash()
|
||||
}
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create message")
|
||||
ctx.SetInternalErrorFlash()
|
||||
ctx.Redirect(fmt.Sprintf("/_/%s/chat/%s", domain, token))
|
||||
return
|
||||
}
|
||||
|
||||
// 保存图片记录(如有)
|
||||
if imageURL != "" {
|
||||
if _, err := db.MessageImages.Create(ctx.Request().Context(), db.CreateMessageImageOptions{
|
||||
MessageID: msg.ID,
|
||||
URL: imageURL,
|
||||
ObjectKey: imageKey,
|
||||
FileName: imageHeader.Filename,
|
||||
FileSize: imageHeader.Size,
|
||||
}); err != nil {
|
||||
logrus.WithError(err).Error("Failed to save message image")
|
||||
ctx.SetInternalErrorFlash()
|
||||
ctx.Redirect(fmt.Sprintf("/_/%s/chat/%s", domain, token))
|
||||
return
|
||||
}
|
||||
imageAttached = true
|
||||
}
|
||||
imageAttached = true
|
||||
|
||||
// 达到上限时关闭对话(幂等,多实例同时执行也安全)
|
||||
if err := db.Conversations.MaybeClose(ctx.Request().Context(), conv.ID, conv.MaxMessages); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user