chore: baseline snapshot before moderation-strictness feature

Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
This commit is contained in:
tamakyi
2026-09-10 10:56:27 +08:00
commit e3e59e7c69
296 changed files with 67458 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
// 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("投票表单字段不存在")