feat: 管理端板块管理与旧版数据导入,补充部署运营文档
- 新增管理端板块管理页面与后端接口(admin_board) - 新增旧版数据导入:legacyimport 服务、导入面板、importusers 命令行工具 - 聊天用户卡片、板块图标等 UI 组件与界面优化 - 补充 about/公告/1Panel 部署等文档 - gitignore 排除 dist/ 构建产物与 .agents/ 本地工具目录
This commit is contained in:
@@ -62,6 +62,7 @@ func InitDB(dsn string) error {
|
||||
&ChatRoom{}, &ChatRoomMember{}, &ChatMessage{},
|
||||
&PointLedger{}, &PostContentUnlock{}, &PostAttachment{}, &PostAttachmentUnlock{},
|
||||
&PostPollVote{}, &PostLotteryEntry{}, &Ad{},
|
||||
&ImportRecord{},
|
||||
); err != nil {
|
||||
return fmt.Errorf("自动迁移失败: %w", err)
|
||||
}
|
||||
@@ -96,8 +97,20 @@ func InitDB(dsn string) error {
|
||||
`).Error; err != nil {
|
||||
return fmt.Errorf("私聊唯一索引创建失败: %w", err)
|
||||
}
|
||||
|
||||
// 昵称全站唯一(忽略大小写,软删用户不占用),与用户名同级约束:
|
||||
// 保证 @提及 昵称→用户名 映射无歧义。应用层另有友好错误提示(service/auth.go UpdateProfile)。
|
||||
// 存量重名会令本索引创建失败(开发期数据可重置)。
|
||||
if err := db.Exec(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_nickname_lower
|
||||
ON users (LOWER(nickname))
|
||||
WHERE deleted_at IS NULL
|
||||
`).Error; err != nil {
|
||||
return fmt.Errorf("昵称唯一索引创建失败(存量数据可能存在重名昵称,请清理或重置数据库): %w", err)
|
||||
}
|
||||
backfillBoardDefaults(db)
|
||||
seedDefaultBoards(db)
|
||||
if err := seedDefaultChatRoom(db); err != nil {
|
||||
if err := EnsureDefaultChatRoom(db); err != nil {
|
||||
return fmt.Errorf("默认群聊大厅初始化失败: %w", err)
|
||||
}
|
||||
if err := ensureDefaultChatMemberships(db); err != nil {
|
||||
@@ -321,13 +334,27 @@ func ensureOwnerRole(db *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// backfillBoardDefaults 为升级前已存在的板块补齐新字段默认值。
|
||||
// 以 post_policy 为空作为「迁移前旧行」的判定标志:仅对这些行一次性回填
|
||||
// post_policy=all、visible=true;后续已被显式设置过的行(含站长隐藏的板块)不会被覆盖。
|
||||
func backfillBoardDefaults(db *gorm.DB) {
|
||||
if err := db.Model(&Board{}).
|
||||
Where("post_policy = '' OR post_policy IS NULL").
|
||||
Updates(map[string]interface{}{
|
||||
"post_policy": BoardPostPolicyAll,
|
||||
"visible": true,
|
||||
}).Error; err != nil {
|
||||
log.Printf("[model] 板块默认值回填失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// seedDefaultBoards 写入默认板块
|
||||
func seedDefaultBoards(db *gorm.DB) {
|
||||
defaults := []Board{
|
||||
{Name: "综合讨论", Description: "什么都可以聊", Icon: "message-circle", SortOrder: 1},
|
||||
{Name: "技术分享", Description: "分享技术心得与问题", Icon: "code", SortOrder: 2},
|
||||
{Name: "问答求助", Description: "提问与解答", Icon: "help-circle", SortOrder: 3},
|
||||
{Name: "闲聊灌水", Description: "轻松闲聊", Icon: "coffee", SortOrder: 4},
|
||||
{Name: "综合讨论", Description: "什么都可以聊", Icon: "message-circle", SortOrder: 1, PostPolicy: BoardPostPolicyAll, Visible: true},
|
||||
{Name: "技术分享", Description: "分享技术心得与问题", Icon: "code", SortOrder: 2, PostPolicy: BoardPostPolicyAll, Visible: true},
|
||||
{Name: "问答求助", Description: "提问与解答", Icon: "help-circle", SortOrder: 3, PostPolicy: BoardPostPolicyAll, Visible: true},
|
||||
{Name: "闲聊灌水", Description: "轻松闲聊", Icon: "coffee", SortOrder: 4, PostPolicy: BoardPostPolicyAll, Visible: true},
|
||||
}
|
||||
for _, b := range defaults {
|
||||
var count int64
|
||||
@@ -338,8 +365,9 @@ func seedDefaultBoards(db *gorm.DB) {
|
||||
}
|
||||
}
|
||||
|
||||
// seedDefaultChatRoom 确保存在唯一的全站大厅(公开默认群)
|
||||
func seedDefaultChatRoom(db *gorm.DB) error {
|
||||
// EnsureDefaultChatRoom 确保存在唯一的全站大厅(公开默认群)。
|
||||
// 导出供注册链路在启动播种时因空库跳过后就地补建(自愈,不依赖重启)。
|
||||
func EnsureDefaultChatRoom(db *gorm.DB) error {
|
||||
var n int64
|
||||
if err := db.Model(&ChatRoom{}).Where("is_default = ?", true).Count(&n).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -116,7 +116,7 @@ type User struct {
|
||||
Username string `gorm:"uniqueIndex;size:128;not null" json:"username"`
|
||||
Email string `gorm:"index;size:128;default:''" json:"-"`
|
||||
Password string `gorm:"size:128;not null" json:"-"`
|
||||
Nickname string `gorm:"size:64" json:"nickname"`
|
||||
Nickname string `gorm:"size:64" json:"nickname"` // 全站唯一:LOWER 函数唯一索引在 db.go InitDB 中创建(应用层校验见 service/auth.go)
|
||||
Avatar string `gorm:"size:512" json:"avatar"`
|
||||
Signature string `gorm:"size:255;default:''" json:"signature"` // 个性签名
|
||||
Role Role `gorm:"size:16;default:user" json:"role"`
|
||||
@@ -161,11 +161,40 @@ type Board struct {
|
||||
Icon string `gorm:"size:64;default:''" json:"icon"`
|
||||
ColorIndex int `gorm:"default:-1" json:"color_index"`
|
||||
SortOrder int `gorm:"default:0" json:"sort_order"`
|
||||
PostPolicy string `gorm:"size:16;default:'all'" json:"post_policy"` // all=全员可发;staff=仅管理员及以上
|
||||
Visible bool `gorm:"default:true" json:"visible"` // false 时不在前台列表/导航展示(仅隐藏入口)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// 板块发帖策略常量
|
||||
const (
|
||||
BoardPostPolicyAll = "all" // 全员可发帖
|
||||
BoardPostPolicyStaff = "staff" // 仅管理员及以上(含管辖该板的版主)可发帖
|
||||
)
|
||||
|
||||
// NormalizeBoardPostPolicy 归一化发帖策略,非法值回落 all
|
||||
func NormalizeBoardPostPolicy(v string) string {
|
||||
switch v {
|
||||
case BoardPostPolicyStaff:
|
||||
return BoardPostPolicyStaff
|
||||
default:
|
||||
return BoardPostPolicyAll
|
||||
}
|
||||
}
|
||||
|
||||
// ImportRecord 外部数据导入去重记录(旧站/WordPress/Typecho 等来源):
|
||||
// 同一来源同一源对象只导入一次,保证「预检→正式导入→重复导入」全程幂等。
|
||||
type ImportRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Source string `gorm:"size:32;not null;uniqueIndex:idx_import_dedupe,priority:1" json:"source"` // jiang13 / wordpress / typecho
|
||||
Kind string `gorm:"size:16;not null;uniqueIndex:idx_import_dedupe,priority:2" json:"kind"` // board / post / comment
|
||||
SourceID string `gorm:"size:64;not null;uniqueIndex:idx_import_dedupe,priority:3" json:"source_id"`
|
||||
NewID uint `json:"new_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// Post 帖子
|
||||
type Post struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
|
||||
Reference in New Issue
Block a user