feat: 管理端板块管理与旧版数据导入,补充部署运营文档

- 新增管理端板块管理页面与后端接口(admin_board)
- 新增旧版数据导入:legacyimport 服务、导入面板、importusers 命令行工具
- 聊天用户卡片、板块图标等 UI 组件与界面优化
- 补充 about/公告/1Panel 部署等文档
- gitignore 排除 dist/ 构建产物与 .agents/ 本地工具目录
This commit is contained in:
2026-09-24 03:37:44 +08:00
parent c1f6a6636b
commit 3e55b5d230
53 changed files with 5078 additions and 409 deletions

View File

@@ -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