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

@@ -392,7 +392,7 @@ func (o *Operations) SendCode(email, purpose, ip string) (int, error) {
return 0, nil
}
}
code := randomID()
code := randomCode()
hash := counterKey(email + ":" + purpose + ":" + code)
e = o.db.Transaction(func(tx *gorm.DB) error {
if e := tx.Create(&model.EmailChallenge{Hash: hash, Email: email, Purpose: purpose, ExpiresAt: time.Now().Add(15 * time.Minute)}).Error; e != nil {
@@ -402,17 +402,60 @@ func (o *Operations) SendCode(email, purpose, ip string) (int, error) {
})
return 0, e
}
const (
codeVerifyMaxAttempts = 5
codeVerifyWindow = 15 * 60 // 秒,与验证码有效期一致
)
// CodeThrottleError 表示验证码校验失败次数超限,携带需等待的秒数。
type CodeThrottleError struct{ Wait int }
func (e *CodeThrottleError) Error() string {
return fmt.Sprintf("验证码错误次数过多,请 %d 秒后重试", e.Wait)
}
func (o *Operations) ConsumeCode(email, purpose, code string) error {
r := o.db.Model(&model.EmailChallenge{}).Where("hash = ? AND used = false AND expires_at > ?", counterKey(strings.ToLower(strings.TrimSpace(email))+":"+purpose+":"+code), time.Now()).Update("used", true)
email = strings.ToLower(strings.TrimSpace(email))
failKey := "verifyfail:" + purpose + ":" + email
// 失败次数限流:窗口内错误次数超限则拒绝,防止 6 位数字码被暴力枚举。
if wait, e := o.verifyFailWait(failKey); e != nil {
return e
} else if wait > 0 {
return &CodeThrottleError{Wait: wait}
}
r := o.db.Model(&model.EmailChallenge{}).Where("hash = ? AND used = false AND expires_at > ?", counterKey(email+":"+purpose+":"+code), time.Now()).Update("used", true)
if r.Error != nil {
return r.Error
}
if r.RowsAffected != 1 {
if _, e := o.Quota(failKey, codeVerifyMaxAttempts, codeVerifyWindow); e != nil {
return e
}
return fmt.Errorf("验证码无效或已过期")
}
// 校验成功,清除失败计数。
o.db.Delete(&model.ActionCounter{}, "key = ?", counterKey(failKey))
return nil
}
// verifyFailWait 读取当前失败计数,若已达上限返回需等待的秒数,不修改计数。
func (o *Operations) verifyFailWait(key string) (int, error) {
var c model.ActionCounter
e := o.db.First(&c, "key = ?", counterKey(key)).Error
if e != nil {
if errors.Is(e, gorm.ErrRecordNotFound) {
return 0, nil
}
return 0, e
}
if time.Now().Before(c.ExpiresAt) && c.Count >= codeVerifyMaxAttempts {
return int(time.Until(c.ExpiresAt).Seconds()) + 1, nil
}
return 0, nil
}
// Probe merged drafts so partial API updates cannot bypass activation validation.
func (o *Operations) ProbeBeforeSave(ctx context.Context, name string, raw json.RawMessage, clear []string) error {
v, _, _, e := o.draft(o.db, name, raw, clear)