安全与可靠性: 修复配额并发、投票事务、密码授权与导出完整性

This commit is contained in:
tamakyi
2026-11-05 12:17:05 +08:00
parent 7636456d4e
commit 25aee35030
18 changed files with 1002 additions and 194 deletions
+17 -41
View File
@@ -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 {