Files
jiu/backend/internal/service/session_cleanup.go
T
wangjia a773a33779 feat: App→网页免登录(一次性换票 SSO)——移动/桌面点链接直达已登录官网
- backend:POST /auth/web-ticket(需登录,签 60s 单次票据,每用户限频)+
  POST /auth/web-ticket/exchange(公开,原子即焚兑换正常登录态,响应同
  /auth/login 另带 shop_code);web_tickets 新表 + user_sessions.kind 列
  (sso 会话:web 平台、refresh 12h 硬到期、不占设备并发配额、设备管理可见
  可踢);清理任务顺带清过期票据;schema.sql/AutoMigrate/testutil 同步
- web:/sso 落地页——兑票写入与 Web 版共享的 localStorage 后跳 redirect;
  redirect 白名单只收本站相对路径(防 open redirect),票据即时从地址栏抹除
- client:launchAuthedWebPath(ref, path) 封装(签票→拼 /sso URL→launchUrl,
  失败降级未登录直开;桌面/移动同一套);授权面板「查看套餐价格」接入

test(backend): web_ticket_test 三用例(兑换+续期/单次即焚/过期拒绝/
sso 不占配额双向验证)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 21:30:08 +08:00

62 lines
2.0 KiB
Go

package service
import (
"log"
"time"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/internal/model"
)
// sessionCleanupInterval 清理任务执行周期。
const sessionCleanupInterval = 24 * time.Hour
// StartSessionCleanup 启动后台清理 goroutine:启动即跑一次,之后每 24h 跑一次。
// 删除已撤销/已过期的会话行与过旧的失败登录记录,防止表无限膨胀、IP/UA 长期滞留。
// retentionDays<=0 时视为关闭清理(直接返回,不启动 goroutine)。
func StartSessionCleanup(db *gorm.DB, retentionDays int) {
if retentionDays <= 0 {
log.Printf("[cleanup] session cleanup disabled (retention_days=%d)", retentionDays)
return
}
go func() {
cleanupOnce(db, retentionDays)
ticker := time.NewTicker(sessionCleanupInterval)
defer ticker.Stop()
for range ticker.C {
cleanupOnce(db, retentionDays)
}
}()
}
// cleanupOnce 执行一轮清理,返回各表删除行数(供测试断言)。
func cleanupOnce(db *gorm.DB, retentionDays int) (sessions, attempts int64) {
cutoff := time.Now().AddDate(0, 0, -retentionDays)
r1 := db.Where(
"(revoked_at IS NOT NULL AND revoked_at < ?) OR (refresh_exp_at IS NOT NULL AND refresh_exp_at < ?)",
cutoff, cutoff,
).Delete(&model.UserSession{})
if r1.Error != nil {
log.Printf("[cleanup] purge user_sessions failed: %v", r1.Error)
}
r2 := db.Where("created_at < ?", cutoff).Delete(&model.LoginAttempt{})
if r2.Error != nil {
log.Printf("[cleanup] purge login_attempts failed: %v", r2.Error)
}
// 一次性网页登录票据:过期即无用(TTL 60s),过期一天后物理清除
if err := db.Where("expires_at < ?", time.Now().AddDate(0, 0, -1)).
Delete(&model.WebTicket{}).Error; err != nil {
log.Printf("[cleanup] purge web_tickets failed: %v", err)
}
if r1.RowsAffected > 0 || r2.RowsAffected > 0 {
log.Printf("[cleanup] purged %d sessions, %d login_attempts (older than %dd)",
r1.RowsAffected, r2.RowsAffected, retentionDays)
}
return r1.RowsAffected, r2.RowsAffected
}