A. SQLite 并发写失败被吞(高概率根因): - DSN 追加 _busy_timeout=5000&_journal_mode=WAL&_synchronous=NORMAL, WAL 下读不阻塞写,消除瞬时 SQLITE_BUSY - UpdateMessagesFlags 不再吞 MarkReadState/MarkFlagged 错误: 出错记日志并返回 error(客户端收到 NO 会重试) - Web MarkRead/Delete 同款静默丢错补日志 B. 客户端 seq 视图错位(集成测试复现后修复): - 真实服务器+脚本客户端测试复现:客户端按日期倒序自编号发 seq 式 STORE 时,旧排序(id ASC)把最旧邮件标为已读 - 规范排序改为 date DESC, id DESC(最新在前),与主流客户端 默认视图一致;buildNewMessageUpdate 改用 seqOf 取真实序号 - 新增 UID STORE / 服务器下发 seq STORE / 自编号 seq STORE 集成测试 注:go-imap v1.2.1 存在库内 *conn.silent() 数据竞争(启用推送后 必然触发),集成测试以 //go:build !race 排除,-race 下由单元测试 覆盖推送逻辑
243 lines
7.9 KiB
Go
243 lines
7.9 KiB
Go
package store
|
||
|
||
import (
|
||
"errors"
|
||
"time"
|
||
|
||
"mail_go/internal/db"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// Common store errors
|
||
var (
|
||
ErrInvalidEmail = errors.New("无效的邮箱地址格式")
|
||
ErrInvalidCredentials = errors.New("用户名或密码错误")
|
||
ErrUserInactive = errors.New("用户已被禁用")
|
||
ErrRecordNotFound = errors.New("记录不存在")
|
||
)
|
||
|
||
// MailStore defines the interface for mail message operations.
|
||
type MailStore interface {
|
||
Create(msg *db.Message) error
|
||
GetByID(id uint) (*db.Message, error)
|
||
ListByUserAndFolder(userID uint, folder string, page, size int) ([]db.Message, int64, error)
|
||
ListAllByUserAndFolder(userID uint, folder string) ([]db.Message, error)
|
||
CountByUserAndFolder(userID uint, folder string) (int64, error)
|
||
MaxIDByUserAndFolder(userID uint, folder string) (uint, error)
|
||
MarkRead(id uint) error
|
||
MarkReadState(id uint, read bool) error
|
||
MarkFlagged(id uint, flagged bool) error
|
||
MoveToFolder(id uint, folder string) error
|
||
Delete(id uint) error
|
||
CountUnread(userID uint, folder string) (int64, error)
|
||
CountByFolder(folder string) (int64, error)
|
||
CountAll() (int64, error)
|
||
TotalSizeByFolder(folder string) (int64, error)
|
||
TotalSize() (int64, error)
|
||
CountByFolderSince(folder string, since time.Time) (int64, error)
|
||
ListAll(page, size int) ([]db.Message, int64, error)
|
||
ListAllByFolder(folder string, page, size int) ([]db.Message, int64, error)
|
||
}
|
||
|
||
// mailStoreGorm implements MailStore using GORM.
|
||
type mailStoreGorm struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// newMailStore creates a new GORM-backed MailStore.
|
||
func newMailStore(database *gorm.DB) MailStore {
|
||
return &mailStoreGorm{db: database}
|
||
}
|
||
|
||
// Create inserts a new message record.
|
||
func (s *mailStoreGorm) Create(msg *db.Message) error {
|
||
return s.db.Create(msg).Error
|
||
}
|
||
|
||
// GetByID retrieves a message by primary key.
|
||
func (s *mailStoreGorm) GetByID(id uint) (*db.Message, error) {
|
||
var msg db.Message
|
||
if err := s.db.First(&msg, id).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
return &msg, nil
|
||
}
|
||
|
||
// ListByUserAndFolder retrieves a paginated list of messages for a user and folder.
|
||
func (s *mailStoreGorm) ListByUserAndFolder(userID uint, folder string, page, size int) ([]db.Message, int64, error) {
|
||
var messages []db.Message
|
||
var total int64
|
||
|
||
query := s.db.Where("user_id = ? AND folder = ?", userID, folder)
|
||
if err := query.Model(&db.Message{}).Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
offset := (page - 1) * size
|
||
if err := query.Order("date DESC").Offset(offset).Limit(size).Find(&messages).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return messages, total, nil
|
||
}
|
||
|
||
// MarkRead sets the IsRead flag to true for a message.
|
||
func (s *mailStoreGorm) MarkRead(id uint) error {
|
||
return s.MarkReadState(id, true)
|
||
}
|
||
|
||
// MarkReadState sets the IsRead flag for a message.
|
||
func (s *mailStoreGorm) MarkReadState(id uint, read bool) error {
|
||
return s.db.Model(&db.Message{}).Where("id = ?", id).Update("is_read", read).Error
|
||
}
|
||
|
||
// MarkFlagged sets the IsFlagged flag for a message.
|
||
func (s *mailStoreGorm) MarkFlagged(id uint, flagged bool) error {
|
||
return s.db.Model(&db.Message{}).Where("id = ?", id).Update("is_flagged", flagged).Error
|
||
}
|
||
|
||
// MoveToFolder changes the folder of a message.
|
||
func (s *mailStoreGorm) MoveToFolder(id uint, folder string) error {
|
||
return s.db.Model(&db.Message{}).Where("id = ?", id).Update("folder", folder).Error
|
||
}
|
||
|
||
// Delete removes a message by ID.
|
||
func (s *mailStoreGorm) Delete(id uint) error {
|
||
return s.db.Delete(&db.Message{}, id).Error
|
||
}
|
||
|
||
// CountUnread returns the count of unread messages for a user in a folder.
|
||
func (s *mailStoreGorm) CountUnread(userID uint, folder string) (int64, error) {
|
||
var count int64
|
||
if err := s.db.Model(&db.Message{}).
|
||
Where("user_id = ? AND folder = ? AND is_read = ?", userID, folder, false).
|
||
Count(&count).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return count, nil
|
||
}
|
||
|
||
// ListAllByUserAndFolder retrieves all messages for a user in a folder without pagination.
|
||
// 按 date DESC, id DESC 排序(最新在前):与主流邮件客户端(Thunderbird、
|
||
// 手机客户端等)默认视图一致,客户端自行按日期编号的 seq 式 STORE 不会
|
||
// 错位标错邮件。所有 IMAP 序号相关路径(Status/ListMessages/推送/seqOf)
|
||
// 共用本排序,保证序号全链路一致。
|
||
func (s *mailStoreGorm) ListAllByUserAndFolder(userID uint, folder string) ([]db.Message, error) {
|
||
var messages []db.Message
|
||
if err := s.db.Where("user_id = ? AND folder = ?", userID, folder).
|
||
Order("date DESC, id DESC").Find(&messages).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
return messages, nil
|
||
}
|
||
|
||
// CountByUserAndFolder returns the total count of messages for a user in a folder.
|
||
func (s *mailStoreGorm) CountByUserAndFolder(userID uint, folder string) (int64, error) {
|
||
var count int64
|
||
if err := s.db.Model(&db.Message{}).
|
||
Where("user_id = ? AND folder = ?", userID, folder).
|
||
Count(&count).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return count, nil
|
||
}
|
||
|
||
// MaxIDByUserAndFolder returns the highest message ID for a user folder.
|
||
func (s *mailStoreGorm) MaxIDByUserAndFolder(userID uint, folder string) (uint, error) {
|
||
var maxID uint
|
||
if err := s.db.Model(&db.Message{}).
|
||
Where("user_id = ? AND folder = ?", userID, folder).
|
||
Select("COALESCE(MAX(id), 0)").
|
||
Scan(&maxID).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return maxID, nil
|
||
}
|
||
|
||
// CountByFolder returns the total count of messages in a given folder.
|
||
func (s *mailStoreGorm) CountByFolder(folder string) (int64, error) {
|
||
var count int64
|
||
if err := s.db.Model(&db.Message{}).Where("folder = ?", folder).Count(&count).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return count, nil
|
||
}
|
||
|
||
// CountAll returns the total count of all messages.
|
||
func (s *mailStoreGorm) CountAll() (int64, error) {
|
||
var count int64
|
||
if err := s.db.Model(&db.Message{}).Count(&count).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return count, nil
|
||
}
|
||
|
||
// TotalSizeByFolder returns the total size (in bytes) of message bodies in a given folder.
|
||
func (s *mailStoreGorm) TotalSizeByFolder(folder string) (int64, error) {
|
||
var total int64
|
||
err := s.db.Model(&db.Message{}).
|
||
Where("folder = ?", folder).
|
||
Select("COALESCE(SUM(LENGTH(text_body) + LENGTH(html_body)), 0)").
|
||
Scan(&total).Error
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return total, nil
|
||
}
|
||
|
||
// TotalSize returns the total size (in bytes) of all message bodies.
|
||
func (s *mailStoreGorm) TotalSize() (int64, error) {
|
||
var total int64
|
||
err := s.db.Model(&db.Message{}).
|
||
Select("COALESCE(SUM(LENGTH(text_body) + LENGTH(html_body)), 0)").
|
||
Scan(&total).Error
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return total, nil
|
||
}
|
||
|
||
// CountByFolderSince returns the count of messages in a folder since a given time.
|
||
func (s *mailStoreGorm) CountByFolderSince(folder string, since time.Time) (int64, error) {
|
||
var count int64
|
||
if err := s.db.Model(&db.Message{}).
|
||
Where("folder = ? AND created_at >= ?", folder, since).
|
||
Count(&count).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return count, nil
|
||
}
|
||
|
||
// ListAll retrieves a paginated list of all messages across all users.
|
||
func (s *mailStoreGorm) ListAll(page, size int) ([]db.Message, int64, error) {
|
||
var messages []db.Message
|
||
var total int64
|
||
|
||
if err := s.db.Model(&db.Message{}).Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
offset := (page - 1) * size
|
||
if err := s.db.Preload("User").Order("date DESC").Offset(offset).Limit(size).Find(&messages).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return messages, total, nil
|
||
}
|
||
|
||
// ListAllByFolder retrieves a paginated list of all messages in a given folder across all users.
|
||
func (s *mailStoreGorm) ListAllByFolder(folder string, page, size int) ([]db.Message, int64, error) {
|
||
var messages []db.Message
|
||
var total int64
|
||
|
||
query := s.db.Where("folder = ?", folder)
|
||
if err := query.Model(&db.Message{}).Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
offset := (page - 1) * size
|
||
if err := s.db.Preload("User").Where("folder = ?", folder).Order("date DESC").Offset(offset).Limit(size).Find(&messages).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return messages, total, nil
|
||
}
|