完善站点运营设置:分路由管理、登录可见评论、邮件模板内联与关闭注册 SSR。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 00:17:00 +08:00
parent acde7ee288
commit 6f054a903c
98 changed files with 8239 additions and 2073 deletions

View File

@@ -0,0 +1,131 @@
package service
import (
"context"
"errors"
"github.com/freefire/jiang13-bbs/model"
"github.com/freefire/jiang13-bbs/version"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"os"
"path/filepath"
"strings"
"time"
)
func (o *Operations) ResetPassword(email, code, password string) error {
cfg, e := o.Security()
if e != nil || !cfg.PasswordReset {
return errors.New("找回密码未开启")
}
if len(password) < 6 || len(password) > 64 {
return errors.New("密码长度须为 6–64 字节")
}
hash, e := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if e != nil {
return e
}
return o.db.Transaction(func(tx *gorm.DB) error {
scoped := *o
scoped.db = tx
if e := scoped.ConsumeCode(email, "reset", code); e != nil {
return e
}
var users []model.User
if e := tx.Where("LOWER(email) = ?", strings.ToLower(strings.TrimSpace(email))).Limit(2).Find(&users).Error; e != nil {
return e
}
if len(users) != 1 {
return errors.New("账号不可用")
}
return tx.Model(&model.User{}).Where("id = ?", users[0].ID).Updates(map[string]any{"password": string(hash), "token_version": gorm.Expr("token_version + 1")}).Error
})
}
func (o *Operations) Diagnostics(ctx context.Context) map[string]any {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
dbState := "正常"
sql, e := o.db.DB()
if e != nil {
dbState = "异常"
} else if sql.PingContext(ctx) != nil {
dbState = "异常"
}
local := "正常"
if o.LocalWritable() != nil {
local = "异常"
}
v, _, e := o.read(o.db, "storage")
storage := "未知"
if e == nil {
storage = "本地:" + local
if v.(*StorageConfig).Backend == "s3" {
storage = "S3:未知(使用存储页测试)"
}
}
var last model.SettingsAudit
result := map[string]any{"state": "未知"}
if o.db.Where("module = ? AND action = ?", "mail", "test").Order("id desc").First(&last).Error == nil {
result = map[string]any{"result": last.Result, "tested_at": last.CreatedAt, "note": "历史测试,不代表持续健康"}
}
var queued int64
queueErr := o.db.WithContext(ctx).Model(&model.MailTask{}).Where("status IN ?", []string{"queued", "retry", "sending"}).Count(&queued).Error
queueState := "正常 · PostgreSQL 持久邮件队列"
if queueErr != nil {
queueState = "异常 · 暂时无法读取邮件队列"
}
return map[string]any{"version": version.Version, "uptime_seconds": int(time.Since(o.Started).Seconds()), "database": dbState, "cache": "未配置独立应用缓存", "tasks": queueState, "queued_mail": queued, "storage": storage, "mail_test": result}
}
// Draft references live in browsers, so unattached database rows are NEVER deleted.
func (o *Operations) ScanTemporary() (map[string]any, error) {
m, e := o.Maintenance()
if e != nil {
return nil, e
}
count, size := 0, int64(0)
cutoff := time.Now().Add(-time.Duration(m.TempDays) * 24 * time.Hour)
for _, dir := range []string{filepath.Join(o.cfg.DataDir, "uploads"), filepath.Join(o.cfg.DataDir, "private")} {
e = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.Type()&os.ModeSymlink != 0 {
return nil
}
if d.IsDir() || !strings.HasSuffix(d.Name(), ".partial") {
return nil
}
info, e := d.Info()
if e != nil {
return e
}
if info.ModTime().Before(cutoff) {
count++
size += info.Size()
}
return nil
})
if e != nil {
return nil, e
}
}
candidates, e := o.temporaryCandidates()
if e != nil {
return nil, e
}
var drafts int64
if e = o.db.Model(&model.PostAttachment{}).Where("post_id = 0").Count(&drafts).Error; e != nil {
return nil, e
}
return map[string]any{"candidates": candidates, "expired_partial_count": count, "expired_partial_bytes": size, "protected_draft_attachments": drafts, "retention_days": m.TempDays, "message": "保留所有已入库图片和草稿附件。只清理受本版本上传锁保护且已过期的候选文件;未知来源的旧 partial 不在线删除。"}, nil
}
func (o *Operations) ClearMailLogs() (int64, error) {
v, _, e := o.read(o.db, "mail")
if e != nil {
return 0, e
}
c := v.(*MailConfig)
r := o.db.Where("status IN ? AND updated_at < ?", []string{"accepted", "failed"}, time.Now().Add(-time.Duration(c.Retention)*24*time.Hour)).Delete(&model.MailTask{})
return r.RowsAffected, r.Error
}