feat(backend): 出入库汇总近30天滚动窗口 + 跨租户安全加固

- summaryBounds:本月/近30天滚动双口径(stock-in/out Summary ?window=rolling30)
- security(SEC-001):新增 ownership.go ensureShopRef 写入侧防线(stock-in/out/finance/
  盘点建单的 warehouse/partner/product 外键归属校验);读取侧全部 Preload 补
  shop_id 作用域,finance Summary JOIN 补租户条件;回归测试 CrossTenantRefs
- security(SEC-002):release 模式 JWT 密钥为空/默认值时拒绝启动
- gofmt 对齐若干 model/cmd 文件

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-03 09:57:52 +08:00
parent 69890b9935
commit ca7595b113
22 changed files with 423 additions and 225 deletions
+14 -14
View File
@@ -4,18 +4,18 @@ import "time"
// ErrorReport 客户端错误上报记录(系统级,不做多租户隔离)
type ErrorReport struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
CreatedAt time.Time `json:"created_at"`
ErrorType string `gorm:"size:30;not null;index" json:"error_type"` // flutter_error / zone_error / caught_exception
AppVersion string `gorm:"size:30" json:"app_version"`
Platform string `gorm:"size:20" json:"platform"` // ios/android/web/windows/macos
Username string `gorm:"size:50;index" json:"username"`
ShopID uint64 `gorm:"index" json:"shop_id"`
ShopNo string `gorm:"size:50" json:"shop_no"`
Role string `gorm:"size:20" json:"role"`
ProductInfo string `gorm:"size:200" json:"product_info"`
ClientIP string `gorm:"size:60" json:"client_ip"`
ErrorMsg string `gorm:"type:text;not null" json:"error_msg"`
StackTrace string `gorm:"type:text" json:"stack_trace"`
OccurredAt time.Time `json:"occurred_at"`
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
CreatedAt time.Time `json:"created_at"`
ErrorType string `gorm:"size:30;not null;index" json:"error_type"` // flutter_error / zone_error / caught_exception
AppVersion string `gorm:"size:30" json:"app_version"`
Platform string `gorm:"size:20" json:"platform"` // ios/android/web/windows/macos
Username string `gorm:"size:50;index" json:"username"`
ShopID uint64 `gorm:"index" json:"shop_id"`
ShopNo string `gorm:"size:50" json:"shop_no"`
Role string `gorm:"size:20" json:"role"`
ProductInfo string `gorm:"size:200" json:"product_info"`
ClientIP string `gorm:"size:60" json:"client_ip"`
ErrorMsg string `gorm:"type:text;not null" json:"error_msg"`
StackTrace string `gorm:"type:text" json:"stack_trace"`
OccurredAt time.Time `json:"occurred_at"`
}
+5 -5
View File
@@ -4,12 +4,12 @@ import "time"
type License struct {
Base
ShopID uint64 `gorm:"not null;index" json:"shop_id"`
LicenseKey string `gorm:"size:768;uniqueIndex" json:"license_key"`
Type string `gorm:"type:enum('trial','monthly','annual','lifetime');default:'trial'" json:"type"`
ShopID uint64 `gorm:"not null;index" json:"shop_id"`
LicenseKey string `gorm:"size:768;uniqueIndex" json:"license_key"`
Type string `gorm:"type:enum('trial','monthly','annual','lifetime');default:'trial'" json:"type"`
// Tier 当前权益档位(标准/pro/max…),随兑换码写入;当前默认 'standard',暂不据此做能力差异。
Tier string `gorm:"size:32;not null;default:'standard'" json:"tier"`
ExpiresAt *time.Time `json:"expires_at"`
Tier string `gorm:"size:32;not null;default:'standard'" json:"tier"`
ExpiresAt *time.Time `json:"expires_at"`
IsActive bool `gorm:"default:true" json:"is_active"`
MaxDevices int `gorm:"default:3" json:"max_devices"`
Features JSON `gorm:"type:json" json:"features,omitempty"`
+8 -8
View File
@@ -3,14 +3,14 @@ package model
import "time"
type LicenseDevice struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
LicenseID uint64 `gorm:"not null;index" json:"license_id"`
ShopID uint64 `gorm:"not null;index" json:"shop_id"`
DeviceID string `gorm:"size:255;not null" json:"device_id"`
DeviceName string `gorm:"size:255" json:"device_name"`
Platform string `gorm:"size:50" json:"platform"` // windows|macos|android|ios|web
ActivatedAt time.Time `gorm:"not null;autoCreateTime" json:"activated_at"`
LastSeenAt time.Time `gorm:"not null;autoUpdateTime" json:"last_seen_at"`
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
LicenseID uint64 `gorm:"not null;index" json:"license_id"`
ShopID uint64 `gorm:"not null;index" json:"shop_id"`
DeviceID string `gorm:"size:255;not null" json:"device_id"`
DeviceName string `gorm:"size:255" json:"device_name"`
Platform string `gorm:"size:50" json:"platform"` // windows|macos|android|ios|web
ActivatedAt time.Time `gorm:"not null;autoCreateTime" json:"activated_at"`
LastSeenAt time.Time `gorm:"not null;autoUpdateTime" json:"last_seen_at"`
}
func (LicenseDevice) TableName() string { return "license_devices" }
+2 -2
View File
@@ -2,8 +2,8 @@ package model
type Partner struct {
TenantBase
Code string `gorm:"size:50" json:"code"`
Name string `gorm:"size:200;not null" json:"name" binding:"required"`
Code string `gorm:"size:50" json:"code"`
Name string `gorm:"size:200;not null" json:"name" binding:"required"`
// 拼音搜索列(同 products):Create/Update 时由 util.ToPinyin 生成,启动回填存量
NamePinyin string `gorm:"size:400;index" json:"-"`
NameInitials string `gorm:"size:100;index" json:"-"`
+10 -10
View File
@@ -2,14 +2,14 @@ package model
type ProductCategory struct {
TenantBase
Name string `gorm:"size:100;not null" json:"name"`
ParentID *uint64 `json:"parent_id"`
SortOrder int `gorm:"default:0" json:"sort_order"`
Name string `gorm:"size:100;not null" json:"name"`
ParentID *uint64 `json:"parent_id"`
SortOrder int `gorm:"default:0" json:"sort_order"`
}
type Product struct {
TenantBase
PublicID string `gorm:"size:36;uniqueIndex" json:"public_id"`
PublicID string `gorm:"size:36;uniqueIndex" json:"public_id"`
// Code 商品编码:同店内唯一。(shop_id, code) 联合唯一索引 uk_shop_code 由 autoMigrate 显式建(见 main.go),
// 不在此用 tag 声明——ShopID 在共用 TenantBase 上,tag 只能建单列索引会破坏多租户隔离。
Code string `gorm:"size:50" json:"code"`
@@ -25,12 +25,12 @@ type Product struct {
// 特有产品的批次属性:每个 product = 一个特有产品/序列号,生产日期/批次归此(单一来源)
ProductionDate *Date `gorm:"type:date" json:"production_date"`
BatchNo string `gorm:"size:50" json:"batch_no"`
MinStock int `gorm:"default:0" json:"min_stock"`
Description string `gorm:"type:text" json:"description"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
NamePinyin string `gorm:"size:400;index" json:"-"`
NameInitials string `gorm:"size:100;index" json:"-"`
MinStock int `gorm:"default:0" json:"min_stock"`
Description string `gorm:"type:text" json:"description"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
NamePinyin string `gorm:"size:400;index" json:"-"`
NameInitials string `gorm:"size:100;index" json:"-"`
// 商品属性字典外键(可空,公开页展示用)
OriginID *uint64 `json:"origin_id"`
+85 -85
View File
@@ -6,21 +6,21 @@ import "time"
type StockInOrder struct {
TenantBase
OrderNo string `gorm:"size:50;uniqueIndex:uk_shop_order_no" json:"order_no"`
Type string `gorm:"size:30;default:'purchase'" json:"type"`
WarehouseID uint64 `gorm:"not null" json:"warehouse_id" binding:"required,min=1"`
PartnerID *uint64 `json:"partner_id"`
OperatorID uint64 `gorm:"not null" json:"operator_id"`
CreatorID *uint64 `json:"creator_id"`
ReviewerID *uint64 `json:"reviewer_id"`
Status string `gorm:"type:enum('draft','pending','approved','rejected');default:'draft'" json:"status"`
OrderDate Date `gorm:"type:date" json:"order_date"`
TotalAmount float64 `gorm:"type:decimal(16,2);default:0" json:"total_amount"`
ReviewedAt *time.Time `json:"reviewed_at"`
OrderNo string `gorm:"size:50;uniqueIndex:uk_shop_order_no" json:"order_no"`
Type string `gorm:"size:30;default:'purchase'" json:"type"`
WarehouseID uint64 `gorm:"not null" json:"warehouse_id" binding:"required,min=1"`
PartnerID *uint64 `json:"partner_id"`
OperatorID uint64 `gorm:"not null" json:"operator_id"`
CreatorID *uint64 `json:"creator_id"`
ReviewerID *uint64 `json:"reviewer_id"`
Status string `gorm:"type:enum('draft','pending','approved','rejected');default:'draft'" json:"status"`
OrderDate Date `gorm:"type:date" json:"order_date"`
TotalAmount float64 `gorm:"type:decimal(16,2);default:0" json:"total_amount"`
ReviewedAt *time.Time `json:"reviewed_at"`
// 退单状态:none=无 / partial=部分退单 / full=已全退(仅 approved 单可退)
ReturnState string `gorm:"size:20;default:'none'" json:"return_state"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
ReturnState string `gorm:"size:20;default:'none'" json:"return_state"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
Items []StockInItem `gorm:"foreignKey:OrderID" json:"items,omitempty"`
Warehouse *Warehouse `gorm:"foreignKey:WarehouseID" json:"warehouse,omitempty"`
@@ -32,25 +32,25 @@ type StockInOrder struct {
type StockInItem struct {
Base
OrderID uint64 `gorm:"not null;index" json:"order_id"`
ShopID uint64 `gorm:"not null" json:"shop_id"`
ProductID uint64 `gorm:"not null" json:"product_id"`
OrderID uint64 `gorm:"not null;index" json:"order_id"`
ShopID uint64 `gorm:"not null" json:"shop_id"`
ProductID uint64 `gorm:"not null" json:"product_id"`
// 历史导入快照列:源明细自带的商品信息,不依赖 product 主数据(product_id 可为 0)。
ProductCode string `gorm:"size:50" json:"product_code"`
ProductName string `gorm:"size:255" json:"product_name"`
Series string `gorm:"size:100" json:"series"`
Spec string `gorm:"size:100" json:"spec"`
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
UnitPrice float64 `gorm:"type:decimal(16,2);default:0" json:"unit_price"`
ProductCode string `gorm:"size:50" json:"product_code"`
ProductName string `gorm:"size:255" json:"product_name"`
Series string `gorm:"size:100" json:"series"`
Spec string `gorm:"size:100" json:"spec"`
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
UnitPrice float64 `gorm:"type:decimal(16,2);default:0" json:"unit_price"`
// 建议售价:仅用于入库建产品时写入 product.SalePrice,不落 stock_in_items 表(gorm:"-")。
SalePrice float64 `gorm:"-" json:"sale_price"`
TotalPrice float64 `gorm:"type:decimal(16,2);default:0" json:"total_price"`
SalePrice float64 `gorm:"-" json:"sale_price"`
TotalPrice float64 `gorm:"type:decimal(16,2);default:0" json:"total_price"`
// 已退数量:0=未退,=Quantity 表示整行已退单(整行退,不做部分数量)
ReturnedQuantity float64 `gorm:"type:decimal(12,3);default:0" json:"returned_quantity"`
BatchNo string `gorm:"size:50" json:"batch_no"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:255" json:"remark"`
BatchNo string `gorm:"size:50" json:"batch_no"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:255" json:"remark"`
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
Order *StockInOrder `gorm:"foreignKey:OrderID" json:"order,omitempty"`
@@ -60,21 +60,21 @@ type StockInItem struct {
type StockOutOrder struct {
TenantBase
OrderNo string `gorm:"size:50;uniqueIndex:uk_shop_order_no" json:"order_no"`
Type string `gorm:"size:30;default:'sale'" json:"type"`
WarehouseID uint64 `gorm:"not null" json:"warehouse_id" binding:"required,min=1"`
PartnerID *uint64 `json:"partner_id"`
OperatorID uint64 `gorm:"not null" json:"operator_id"`
CreatorID *uint64 `json:"creator_id"`
ReviewerID *uint64 `json:"reviewer_id"`
Status string `gorm:"type:enum('draft','pending','approved','rejected');default:'draft'" json:"status"`
OrderDate Date `gorm:"type:date" json:"order_date"`
TotalAmount float64 `gorm:"type:decimal(16,2);default:0" json:"total_amount"`
ReviewedAt *time.Time `json:"reviewed_at"`
OrderNo string `gorm:"size:50;uniqueIndex:uk_shop_order_no" json:"order_no"`
Type string `gorm:"size:30;default:'sale'" json:"type"`
WarehouseID uint64 `gorm:"not null" json:"warehouse_id" binding:"required,min=1"`
PartnerID *uint64 `json:"partner_id"`
OperatorID uint64 `gorm:"not null" json:"operator_id"`
CreatorID *uint64 `json:"creator_id"`
ReviewerID *uint64 `json:"reviewer_id"`
Status string `gorm:"type:enum('draft','pending','approved','rejected');default:'draft'" json:"status"`
OrderDate Date `gorm:"type:date" json:"order_date"`
TotalAmount float64 `gorm:"type:decimal(16,2);default:0" json:"total_amount"`
ReviewedAt *time.Time `json:"reviewed_at"`
// 退单状态:none / partial / full
ReturnState string `gorm:"size:20;default:'none'" json:"return_state"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
ReturnState string `gorm:"size:20;default:'none'" json:"return_state"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
Items []StockOutItem `gorm:"foreignKey:OrderID" json:"items,omitempty"`
Warehouse *Warehouse `gorm:"foreignKey:WarehouseID" json:"warehouse,omitempty"`
@@ -86,25 +86,25 @@ type StockOutOrder struct {
type StockOutItem struct {
Base
OrderID uint64 `gorm:"not null;index" json:"order_id"`
ShopID uint64 `gorm:"not null" json:"shop_id"`
ProductID uint64 `gorm:"not null" json:"product_id"`
OrderID uint64 `gorm:"not null;index" json:"order_id"`
ShopID uint64 `gorm:"not null" json:"shop_id"`
ProductID uint64 `gorm:"not null" json:"product_id"`
// 历史导入快照列:源明细自带的商品信息,不依赖 product 主数据(product_id 可为 0)。
ProductCode string `gorm:"size:50" json:"product_code"`
ProductName string `gorm:"size:255" json:"product_name"`
Series string `gorm:"size:100" json:"series"`
Spec string `gorm:"size:100" json:"spec"`
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
UnitPrice float64 `gorm:"type:decimal(16,2);default:0" json:"unit_price"`
ProductCode string `gorm:"size:50" json:"product_code"`
ProductName string `gorm:"size:255" json:"product_name"`
Series string `gorm:"size:100" json:"series"`
Spec string `gorm:"size:100" json:"spec"`
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
UnitPrice float64 `gorm:"type:decimal(16,2);default:0" json:"unit_price"`
// 售价:出库实际销售单价(可编辑),应收账款按 售价×数量 计;与 UnitPrice(入库成本) 区分
SalePrice float64 `gorm:"type:decimal(16,2);default:0" json:"sale_price"`
TotalPrice float64 `gorm:"type:decimal(16,2);default:0" json:"total_price"`
SalePrice float64 `gorm:"type:decimal(16,2);default:0" json:"sale_price"`
TotalPrice float64 `gorm:"type:decimal(16,2);default:0" json:"total_price"`
// 已退数量:0=未退,=Quantity 表示整行已退单
ReturnedQuantity float64 `gorm:"type:decimal(12,3);default:0" json:"returned_quantity"`
BatchNo string `gorm:"size:50" json:"batch_no"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:255" json:"remark"`
BatchNo string `gorm:"size:50" json:"batch_no"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:255" json:"remark"`
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
}
@@ -112,30 +112,30 @@ type StockOutItem struct {
// -------- 实时库存(批次模式:每条记录代表一个批次/批次) --------
type Inventory struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
ShopID uint64 `gorm:"not null" json:"shop_id"`
WarehouseID *uint64 `json:"warehouse_id"`
ProductID *uint64 `json:"product_id"`
StockInItemID *uint64 `json:"stock_in_item_id"`
InventoryCheckID *uint64 `json:"inventory_check_id"`
Quantity float64 `gorm:"type:decimal(12,3);not null;default:0" json:"quantity"`
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
ShopID uint64 `gorm:"not null" json:"shop_id"`
WarehouseID *uint64 `json:"warehouse_id"`
ProductID *uint64 `json:"product_id"`
StockInItemID *uint64 `json:"stock_in_item_id"`
InventoryCheckID *uint64 `json:"inventory_check_id"`
Quantity float64 `gorm:"type:decimal(12,3);not null;default:0" json:"quantity"`
// Snapshot fields: copied from product/warehouse/stock-in at approval time.
// They reflect the state at the moment of stock-in and are NOT updated when the
// referenced product or warehouse record is later modified.
ProductCode string `gorm:"size:50" json:"product_code"`
ProductName string `gorm:"size:200" json:"product_name"`
Series string `gorm:"size:100" json:"series"`
Spec string `gorm:"size:100" json:"spec"`
Unit string `gorm:"size:20" json:"unit"`
WarehouseName string `gorm:"size:100" json:"warehouse_name"`
UnitPrice *float64 `gorm:"type:decimal(16,2)" json:"unit_price"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
BatchNo string `gorm:"size:50" json:"batch_no"`
SupplierName string `gorm:"size:200" json:"supplier_name"`
Remark string `gorm:"size:500" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `gorm:"index" json:"-"`
ProductCode string `gorm:"size:50" json:"product_code"`
ProductName string `gorm:"size:200" json:"product_name"`
Series string `gorm:"size:100" json:"series"`
Spec string `gorm:"size:100" json:"spec"`
Unit string `gorm:"size:20" json:"unit"`
WarehouseName string `gorm:"size:100" json:"warehouse_name"`
UnitPrice *float64 `gorm:"type:decimal(16,2)" json:"unit_price"`
ProductionDate *Date `gorm:"type:date" json:"production_date"`
BatchNo string `gorm:"size:50" json:"batch_no"`
SupplierName string `gorm:"size:200" json:"supplier_name"`
Remark string `gorm:"size:500" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `gorm:"index" json:"-"`
StockInItem *StockInItem `gorm:"foreignKey:StockInItemID" json:"stock_in_item,omitempty"`
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
@@ -166,12 +166,12 @@ type InventoryLog struct {
type InventoryCheck struct {
TenantBase
CheckNo string `gorm:"size:50;uniqueIndex:uk_shop_check_no" json:"check_no"`
WarehouseID uint64 `gorm:"not null" json:"warehouse_id"`
OperatorID uint64 `gorm:"not null" json:"operator_id"`
Status string `gorm:"type:enum('draft','completed');default:'draft'" json:"status"`
CheckDate Date `gorm:"type:date" json:"check_date"`
Remark string `gorm:"size:500" json:"remark"`
CheckNo string `gorm:"size:50;uniqueIndex:uk_shop_check_no" json:"check_no"`
WarehouseID uint64 `gorm:"not null" json:"warehouse_id"`
OperatorID uint64 `gorm:"not null" json:"operator_id"`
Status string `gorm:"type:enum('draft','completed');default:'draft'" json:"status"`
CheckDate Date `gorm:"type:date" json:"check_date"`
Remark string `gorm:"size:500" json:"remark"`
Items []InventoryCheckItem `gorm:"foreignKey:CheckID" json:"items,omitempty"`
}
+13 -13
View File
@@ -5,26 +5,26 @@ 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"`
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"`
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
RevokedReason string `gorm:"size:30" json:"revoked_reason,omitempty"` // kicked|logout|admin|disabled|reuse|pwd_reset
RevokedBy *uint64 `gorm:"column:revoked_by" json:"revoked_by,omitempty"` // 吊销操作人 user_id;系统/自助吊销为 NULL
RefreshExpAt time.Time `json:"refresh_exp_at"`
}