// 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 VoteFormFields VoteFormFieldsStore // CustomFieldMarker 参与端"其他/自定义选项"的保留标记值(radio/select/checkbox 字段)。 // 选中该值时,后端从 field_{id}_custom 读取参与者填写的文本;预设选项不允许使用该保留值。 const CustomFieldMarker = "__tb_custom_option__" // 表单字段类型(type=form 时使用) const ( VoteFormFieldTypeText = "text" // 单行文本 VoteFormFieldTypeTextarea = "textarea" // 多行文本 VoteFormFieldTypeSelect = "select" // 下拉单选 VoteFormFieldTypeRadio = "radio" // 单选 VoteFormFieldTypeCheckbox = "checkbox" // 多选 VoteFormFieldTypeNumber = "number" // 数字 VoteFormFieldTypeRating = "rating" // 评分 VoteFormFieldTypeRank = "rank" // 排序(对预置选项排 1..N) ) type VoteFormFieldsStore interface { Create(ctx context.Context, opts CreateVoteFormFieldOptions) (*VoteFormField, error) BulkCreate(ctx context.Context, voteID uint, fields []CreateVoteFormFieldOptions) ([]*VoteFormField, error) ListByVoteID(ctx context.Context, voteID uint) ([]*VoteFormField, error) ReplaceByVoteID(ctx context.Context, voteID uint, fields []CreateVoteFormFieldOptions) error CountByVoteID(ctx context.Context, voteID uint) (int64, error) DeleteByVoteID(ctx context.Context, voteID uint) error DeleteByID(ctx context.Context, id uint) error } func NewVoteFormFieldsStore(db *gorm.DB) VoteFormFieldsStore { return &voteFormFields{db} } type voteFormFields struct { *gorm.DB } // VoteFormField 表单字段定义表:结构化表单(type=form)的字段配置。 type VoteFormField struct { dbutil.Model VoteID uint `gorm:"index:idx_form_field_vote" json:"vote_id"` FieldType string `gorm:"size:20" json:"field_type"` Label string `gorm:"size:100" json:"label"` Required bool `gorm:"default:false" json:"required"` MaxLength int `gorm:"default:0" json:"max_length"` // 0 = 不限制(字数限制) Options string `gorm:"type:text" json:"options"` // JSON 数组(select/radio/checkbox 用) SortOrder int `gorm:"default:0" json:"sort_order"` AllowCustom bool `gorm:"default:false" json:"allow_custom"` // 允许参与者补充自定义选项(radio/select/checkbox) } type CreateVoteFormFieldOptions struct { VoteID uint FieldType string Label string Required bool MaxLength int Options string SortOrder int AllowCustom bool } func (db *voteFormFields) Create(ctx context.Context, opts CreateVoteFormFieldOptions) (*VoteFormField, error) { field := &VoteFormField{ VoteID: opts.VoteID, FieldType: opts.FieldType, Label: opts.Label, Required: opts.Required, MaxLength: opts.MaxLength, Options: opts.Options, SortOrder: opts.SortOrder, AllowCustom: opts.AllowCustom, } if err := db.WithContext(ctx).Create(field).Error; err != nil { return nil, errors.Wrap(err, "create vote form field") } return field, nil } // BulkCreate 批量创建表单字段(箱主一次提交全部字段配置)。 func (db *voteFormFields) BulkCreate(ctx context.Context, voteID uint, fields []CreateVoteFormFieldOptions) ([]*VoteFormField, error) { formFields := make([]*VoteFormField, 0, len(fields)) for _, f := range fields { formFields = append(formFields, &VoteFormField{ VoteID: voteID, FieldType: f.FieldType, Label: f.Label, Required: f.Required, MaxLength: f.MaxLength, Options: f.Options, SortOrder: f.SortOrder, AllowCustom: f.AllowCustom, }) } if len(formFields) == 0 { return formFields, nil } if err := db.WithContext(ctx).Create(&formFields).Error; err != nil { return nil, errors.Wrap(err, "bulk create vote form fields") } return formFields, nil } // ListByVoteID 获取某投票的全部表单字段(按 SortOrder 升序)。 func (db *voteFormFields) ListByVoteID(ctx context.Context, voteID uint) ([]*VoteFormField, error) { var fields []*VoteFormField if err := db.WithContext(ctx). Where("vote_id = ?", voteID). Order("sort_order ASC, id ASC"). Find(&fields).Error; err != nil { return nil, errors.Wrap(err, "list vote form fields by vote id") } return fields, nil } // CountByVoteID 统计某投票的表单字段数量(发布前完整性校验用)。 func (db *voteFormFields) CountByVoteID(ctx context.Context, voteID uint) (int64, error) { var count int64 if err := db.WithContext(ctx).Model(&VoteFormField{}).Where("vote_id = ?", voteID).Count(&count).Error; err != nil { return 0, errors.Wrap(err, "count vote form fields by vote id") } return count, nil } // ReplaceByVoteID 在单个事务内替换投票的全部表单字段(先删后建),保证原子性。 // 仅在草稿状态编辑时调用。 func (db *voteFormFields) ReplaceByVoteID(ctx context.Context, voteID uint, fields []CreateVoteFormFieldOptions) error { return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("vote_id = ?", voteID).Delete(&VoteFormField{}).Error; err != nil { return err } if len(fields) == 0 { return nil } formFields := make([]*VoteFormField, 0, len(fields)) for _, f := range fields { formFields = append(formFields, &VoteFormField{ VoteID: voteID, FieldType: f.FieldType, Label: f.Label, Required: f.Required, MaxLength: f.MaxLength, Options: f.Options, SortOrder: f.SortOrder, AllowCustom: f.AllowCustom, }) } return tx.Create(&formFields).Error }) } // DeleteByVoteID 删除投票时级联清理全部表单字段。 func (db *voteFormFields) DeleteByVoteID(ctx context.Context, voteID uint) error { if err := db.WithContext(ctx).Where("vote_id = ?", voteID).Delete(&VoteFormField{}).Error; err != nil { return errors.Wrap(err, "delete vote form fields by vote id") } return nil } func (db *voteFormFields) DeleteByID(ctx context.Context, id uint) error { res := db.WithContext(ctx).Delete(&VoteFormField{}, id) if res.Error != nil { return errors.Wrap(res.Error, "delete vote form field") } if res.RowsAffected == 0 { return ErrVoteFormFieldNotExist } return nil } var ErrVoteFormFieldNotExist = errors.New("投票表单字段不存在")