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("账号不可用") } if e := tx.Model(&model.User{}).Where("id = ?", users[0].ID).Updates(map[string]any{ "password": string(hash), "token_version": gorm.Expr("token_version + 1"), }).Error; e != nil { return e } return tx.Model(&model.RefreshToken{}).Where("user_id = ? AND revoked = ?", users[0].ID, false). Updates(map[string]any{"revoked": true, "token_cipher": ""}).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 }