Files
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

50 lines
2.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package model
import "time"
// UserSession 服务端登录会话。JWT 的 sid claim 指向此表一行,
// 用于支持登出/踢人/在线状态监控(JWT 本身无状态,无法撤销)。
type UserSession struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
ShopID uint64 `gorm:"not null;index:idx_session_shop_user" json:"shop_id"`
UserID uint64 `gorm:"not null;index:idx_session_shop_user" json:"user_id"`
SID string `gorm:"column:sid;size:64;not null;uniqueIndex" json:"sid"` // 嵌入 JWT
DeviceID string `gorm:"size:255" json:"device_id"`
DeviceName string `gorm:"size:255" json:"device_name"`
Platform string `gorm:"size:50" json:"platform"` // windows|macos|linux|android|ios|web
PlatformClass string `gorm:"size:20;index" json:"platform_class"` // desktop|mobile|web
IP string `gorm:"size:64" json:"ip"`
UserAgent string `gorm:"size:512" json:"user_agent"`
// RefreshJTI 当前有效 refresh token 的 jti,用于「轮换 + 重用检测」:
// 每次续期轮换此值,若 refresh 携带的 jti 与之不符即判定为旧 token 重放(盗用),吊销整条会话。
RefreshJTI string `gorm:"column:refresh_jti;size:64" json:"-"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
// LastSeenAt 手工维护(Login 建行赋值、心跳/refresh 显式 Update)。
// 故意用 autoCreateTime(建行给默认、之后不被 ORM 自动改);切勿改成 autoUpdateTime
// 否则 revoke/cleanup 等任意 Updates 都会把已撤销会话误刷成「刚活跃」。
LastSeenAt time.Time `gorm:"autoCreateTime" json:"last_seen_at"`
RevokedAt *time.Time `gorm:"index" json:"revoked_at,omitempty"`
RevokedReason string `gorm:"size:30" json:"revoked_reason,omitempty"` // kicked|logout|admin|disabled|reuse|pwd_reset|relogin
// Kind 会话种类:login=正常登录;sso=App 换票的网页短时会话
//(不计设备并发配额,refresh 短时效)。
Kind string `gorm:"size:10;default:'login'" json:"kind"`
RevokedBy *uint64 `gorm:"column:revoked_by" json:"revoked_by,omitempty"` // 吊销操作人 user_id;系统/自助吊销为 NULL
RefreshExpAt time.Time `json:"refresh_exp_at"`
}
func (UserSession) TableName() string { return "user_sessions" }
// PlatformClass 把具体平台归类,用于按类限并发。
func PlatformClass(platform string) string {
switch platform {
case "android", "ios":
return "mobile"
case "web":
return "web"
case "windows", "macos", "linux":
return "desktop"
default:
return "desktop" // 未知平台按桌面端处理
}
}