// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package db import ( "context" "github.com/pkg/errors" "gorm.io/gorm" "github.com/tamakyi/TamaBox/internal/dbutil" ) var VoteOptions VoteOptionsStore type VoteOptionsStore interface { Create(ctx context.Context, opts CreateVoteOptionOptions) (*VoteOption, error) BulkCreate(ctx context.Context, voteID uint, contents []string) ([]*VoteOption, error) ListByVoteID(ctx context.Context, voteID uint) ([]*VoteOption, error) ReplaceByVoteID(ctx context.Context, voteID uint, contents []string) error DeleteByVoteID(ctx context.Context, voteID uint) error DeleteByID(ctx context.Context, id uint) error CountByVoteID(ctx context.Context, voteID uint) (int64, error) CountByVoteIDs(ctx context.Context, voteIDs []uint) (map[uint]int64, error) } func NewVoteOptionsStore(db *gorm.DB) VoteOptionsStore { return &voteOptions{db} } type voteOptions struct { *gorm.DB } // VoteOption 选项表:单选/多选/评分/排序共用。 type VoteOption struct { dbutil.Model VoteID uint `gorm:"index:idx_option_vote" json:"vote_id"` Content string `gorm:"size:200" json:"content"` SortOrder int `gorm:"default:0" json:"sort_order"` } type CreateVoteOptionOptions struct { VoteID uint Content string SortOrder int } func (db *voteOptions) Create(ctx context.Context, opts CreateVoteOptionOptions) (*VoteOption, error) { option := &VoteOption{ VoteID: opts.VoteID, Content: opts.Content, SortOrder: opts.SortOrder, } if err := db.WithContext(ctx).Create(option).Error; err != nil { return nil, errors.Wrap(err, "create vote option") } return option, nil } // BulkCreate 批量创建选项(用于箱主一次提交全部选项)。 func (db *voteOptions) BulkCreate(ctx context.Context, voteID uint, contents []string) ([]*VoteOption, error) { options := make([]*VoteOption, 0, len(contents)) for i, content := range contents { options = append(options, &VoteOption{ VoteID: voteID, Content: content, SortOrder: i, }) } if len(options) == 0 { return options, nil } if err := db.WithContext(ctx).Create(&options).Error; err != nil { return nil, errors.Wrap(err, "bulk create vote options") } return options, nil } // ListByVoteID 获取某投票的全部选项(按 SortOrder 升序,保证选项顺序稳定)。 func (db *voteOptions) ListByVoteID(ctx context.Context, voteID uint) ([]*VoteOption, error) { var options []*VoteOption if err := db.WithContext(ctx). Where("vote_id = ?", voteID). Order("sort_order ASC, id ASC"). Find(&options).Error; err != nil { return nil, errors.Wrap(err, "list vote options by vote id") } return options, nil } // ReplaceByVoteID 在单个事务内替换投票的全部选项(先删后建),保证原子性。 // 仅在草稿状态编辑时调用(发布后不可改选项,避免破坏历史投票归因)。 func (db *voteOptions) ReplaceByVoteID(ctx context.Context, voteID uint, contents []string) error { return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("vote_id = ?", voteID).Delete(&VoteOption{}).Error; err != nil { return err } if len(contents) == 0 { return nil } options := make([]*VoteOption, 0, len(contents)) for i, content := range contents { options = append(options, &VoteOption{VoteID: voteID, Content: content, SortOrder: i}) } return tx.Create(&options).Error }) } // DeleteByVoteID 删除投票时级联清理全部选项。 func (db *voteOptions) DeleteByVoteID(ctx context.Context, voteID uint) error { if err := db.WithContext(ctx).Where("vote_id = ?", voteID).Delete(&VoteOption{}).Error; err != nil { return errors.Wrap(err, "delete vote options by vote id") } return nil } func (db *voteOptions) DeleteByID(ctx context.Context, id uint) error { res := db.WithContext(ctx).Delete(&VoteOption{}, id) if res.Error != nil { return errors.Wrap(res.Error, "delete vote option") } if res.RowsAffected == 0 { return ErrVoteOptionNotExist } return nil } var ErrVoteOptionNotExist = errors.New("投票选项不存在") func (db *voteOptions) CountByVoteID(ctx context.Context, voteID uint) (int64, error) { var count int64 if err := db.WithContext(ctx).Model(&VoteOption{}).Where("vote_id = ?", voteID).Count(&count).Error; err != nil { return 0, errors.Wrap(err, "count vote options by vote id") } return count, nil } // CountByVoteIDs 一次查询批量统计多个投票的选项数(替代逐投票 N 次 COUNT,消除 N+1)。 func (db *voteOptions) CountByVoteIDs(ctx context.Context, voteIDs []uint) (map[uint]int64, error) { out := make(map[uint]int64, len(voteIDs)) if len(voteIDs) == 0 { return out, nil } type row struct { VoteID uint Cnt int64 } var rows []row if err := db.WithContext(ctx).Model(&VoteOption{}). Select("vote_id, COUNT(id) AS cnt"). Where("vote_id IN ?", voteIDs). Group("vote_id"). Find(&rows).Error; err != nil { return nil, errors.Wrap(err, "count vote options by vote ids") } for _, r := range rows { out[r.VoteID] = r.Cnt } return out, nil }