完善站点运营设置:分路由管理、登录可见评论、邮件模板内联与关闭注册 SSR。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
131
backend/service/operations_maintenance.go
Normal file
131
backend/service/operations_maintenance.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user