Documentation
¶
Overview ¶
suffix.go — 扩展名推导(仪表盘「文件类型分布」用)。
⚠️ 2026-10-26:本文件源于一次兼容层退役的漏网残留——管理台统计曾直接查 **已退役的 suffix 列**(该列恒空 ⇒ widget 早就是死的;退役后变成「SQL 报错被 `_` 吞掉」⇒ 恒空且**零日志**)。⇒ 改为从**现役列**现算扩展名。
口径与 model.FileCode.DisplayName() 对齐:文件分享的**原始文件名**在 `text` (直传/分片/预签名/本地导入四个通道都写它)⇒ 扩展名 = 最后一个点之后的尾巴。
Index ¶
- Constants
- func FileExtension(name string) string
- func IsNotReady(fc *FileCode) bool
- func ValidShareStatus(s string) bool
- type AdminOperationLog
- type AdminOperationLogQuery
- type ChunkQuery
- type ChunkStats
- type ChunkUpdate
- type FileCode
- type FileCodeFile
- type FileCodeQuery
- type FileCodeStats
- type FileCodeUpdate
- type FilePreview
- type FileRequest
- type Notify
- type PickupCharge
- type SystemConfigRecord
- type TransferLog
- type TransferLogQuery
- type UploadChunk
- type User
- type UserAPIKey
- type UserQuery
- type UserResp
- type UserStats
Constants ¶
const ( StatusNormal = "normal" StatusBlocked = "blocked" StatusPendingReview = "pending_review" )
分享管控状态(治理 2026-10-03)。与软删除正交:
- normal 正常(默认;老库 AutoMigrate 补列后全部回填 normal)
- blocked 管理员禁用(不可取件/下载,可恢复,记录留证)
- pending_review 内容待审核(moderation 命中且策略为 pending 时置入,审核通过恢复 normal)
const PickupCodeLength = 6
PickupCodeLength 取件码长度(6 位)——**唯一真相源**。
此前三处各写一份(app/anonymous 的 codeLength、app/share 的 pickupCodeLength、 app/mcp 的字面量 6)⇒ 任一处改长度即静默漂移(分享域兜底失效 / MCP 解析失效)。
Variables ¶
This section is empty.
Functions ¶
func FileExtension ¶ added in v0.32.16
FileExtension 取文件名的扩展名(**含前导点**,如 ".pdf")。
形态约定:与被退役的 suffix 列的旧形态一致(前端 `suffix.replace(/^\./, ”)` 照常工作)⇒ 仪表盘的「文件类型分布」不用改前端。
无扩展名("README"、"archive.")⇒ 返回空串(调用方跳过)。 含路径("a/b/c.pdf")⇒ 先取末段再判扩展名。
func IsNotReady ¶ added in v0.32.0
IsNotReady 判定记录是否为「未完成上传」的占位分享:既无文件路径、也非文本分享。
与匿名通道的判定式逐字一致(!IsTextShare() && GetFilePath() == "")。 判定式必须**只有一处**(分散在多域会各自漂移 ⇒ 同一份占位分享在不同端点 给出不同结论)。
Types ¶
type AdminOperationLog ¶
type AdminOperationLog struct {
gorm.Model
Action string `gorm:"size:100;index" json:"action"`
Target string `gorm:"size:255" json:"target"`
Success bool `json:"success"`
Message string `gorm:"type:text" json:"message"`
ActorID *uint `gorm:"index" json:"actor_id"`
ActorName string `gorm:"size:100" json:"actor_name"`
IP string `gorm:"size:45" json:"ip"`
LatencyMs int64 `json:"latency_ms"`
}
AdminOperationLog 记录后台运维操作 Action: 具体操作标识,例如 maintenance.clean_temp Target: 操作影响的目标对象描述 Success: 操作是否成功 Message: 可读的描述信息 Actor: 操作者信息 LatencyMs: 操作耗时(毫秒,可选)
type AdminOperationLogQuery ¶
type AdminOperationLogQuery struct {
Action string
Actor string
Success *bool
Page int
PageSize int
}
AdminOperationLogQuery 查询后台操作日志 Success: nil 表示忽略; true/false 表示按结果过滤
type ChunkQuery ¶
type ChunkQuery struct {
UploadID string `json:"upload_id"`
ChunkIndex *int `json:"chunk_index"`
Status string `json:"status"`
Completed *bool `json:"completed"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
ChunkQuery 分片查询条件
type ChunkStats ¶
type ChunkStats struct {
gorm.Model
TotalChunks int64 `json:"total_chunks"`
CompletedChunks int64 `json:"completed_chunks"`
PendingChunks int64 `json:"pending_chunks"`
FailedChunks int64 `json:"failed_chunks"`
}
ChunkStats 分片统计查询结果
type ChunkUpdate ¶
type ChunkUpdate struct {
gorm.Model
ChunkHash *string `json:"chunk_hash"`
Completed *bool `json:"completed"`
RetryCount *int `json:"retry_count"`
LastError *string `json:"last_error"`
Status *string `json:"status"`
UpdatedAt *time.Time `json:"updated_at"`
}
ChunkUpdate 分片更新数据
type FileCode ¶
type FileCode struct {
gorm.Model
Code string `gorm:"uniqueIndex;size:255" json:"code"`
UUIDFileName string `gorm:"size:255" json:"uuid_file_name"`
FilePath string `gorm:"size:255" json:"file_path"`
Size int64 `gorm:"default:0" json:"size"`
Text string `gorm:"type:text" json:"text"`
// Note 文件备注(可空):随分享展示给取件人(2026-10-12,文件+文本一并投递)
Note string `gorm:"type:text" json:"note"`
ExpiredAt *time.Time `gorm:"index:idx_file_codes_status_expired,priority:2" json:"expired_at"`
ExpiredCount int `gorm:"default:0" json:"expired_count"` // 剩余可取次数:-1=无限, 0=已耗尽, >0=剩余
UsedCount int `gorm:"default:0" json:"used_count"`
// Status 管控状态,默认 normal;blocked/pending_review 拒绝取件(索引覆盖管理端按状态筛选)
Status string `gorm:"size:20;default:'normal';index:idx_file_codes_status_expired,priority:1" json:"status"`
FileHash string `gorm:"size:64" json:"file_hash"`
IsChunked bool `gorm:"default:false" json:"is_chunked"`
UploadID string `gorm:"size:36" json:"upload_id"`
// 新增:用户认证相关字段
UserID *uint `gorm:"index" json:"user_id"` // 上传用户ID,为null表示匿名上传
UploadType string `gorm:"size:20;default:'anonymous'" json:"upload_type"` // anonymous, authenticated
RequireAuth bool `gorm:"default:false" json:"require_auth"` // 是否需要密码才能下载
PasswordHash string `gorm:"size:255" json:"-"` // 取件密码的 bcrypt 哈希(json:"-" 不外泄)
OwnerIP string `gorm:"size:45" json:"owner_ip"` // 上传者IP地址
// Encrypted 端到端客户端加密(P1 E2E):true 时存储的是密文,密钥经分享链接
// #fragment 传递、从不落服务端;服务端零知识(预览/服务端 zip 打包不适用)
Encrypted bool `gorm:"default:false" json:"encrypted"`
// 取件追踪(软删除字段 gorm.Model.DeletedAt 已自带)
ViewerIP string `gorm:"size:45" json:"viewer_ip"` // 最近一次取件人IP
ViewerAt *time.Time `json:"viewer_at"` // 最近一次取件时间
ViewerCount int `gorm:"default:0" json:"viewer_count"` // 累计取件次数
LastNotifiedAt *time.Time `json:"last_notified_at"` // 最近一次给 owner 发通知的时间(用于去重)
// PickupCode 6 位取件码(2026-10-08 起落库持久化,"只保留 6 位码"呈现):
// - 本列为真相源;Redis/memkv 映射降级为加速缓存(TTL=分享过期时间),
// 单机内存模式重启后经 GetByPickupCode 回退解析,取件码不再失联
// - 永久分享同样铸造(KV 无法表达永久 TTL,DB 无此约束)
// - nil=未铸造(E2E 密文分享/历史数据);可空唯一索引允许多行 NULL
PickupCode *string `gorm:"uniqueIndex;size:8" json:"pickup_code,omitempty"`
}
FileCode 文件代码模型
func (*FileCode) DisplayName ¶ added in v0.14.5
DisplayName 返回分享条目的展示文件名(下载 Content-Disposition、管理/用户 列表、取件元信息等所有呈现位统一从本方法取名)。
口径统一(2026-10-26 兼容层退役):Text(原始文件名)→ UUIDFileName(存储名) → 路径末段。此前本方法以 UUIDFileName 优先,而子表 FileCodeFile.DisplayName 以 FileName 优先——同一份文件在管理端列表与取件页显示两个名字(前者显示 "56781532-….txt",后者显示 "m1.txt")。Prefix+Suffix 分支是旧直传布局的 原始名拆分,库中已无任何行使用,随死列一并退役。 纯文本分享(Text=正文且无文件路径)返回空串——文本没有文件名语义,兜底由调用方定。
func (*FileCode) GetFilePath ¶
GetFilePath 获取文件路径(存储相对路径,相对存储根)。
口径统一(2026-10-26 兼容层退役):file_path 恒为「含唯一文件名的完整相对路径」, 所有写入通道(直传/分片/预签名/匿名取件/多文件/本地导入)一致。此前本方法并存 两种形态——「目录 + UUIDFileName」与「file_path 直含完整路径」——靠 strings.Contains 猜走哪条分支;同一列两种语义导致「双重路径」拼装(见 anonymous.BindFilePath 的 清空兜底注释),且匿名占位记录(file_path 空、uuid_file_name 为展示名)会拼出 不存在的路径。列语义既已统一,本方法退化为单一取值。
func (*FileCode) IsBlockedShare ¶
IsBlockedShare 分享是否处于管控拒绝态(blocked / pending_review)
func (*FileCode) IsCountExhausted ¶ added in v0.32.0
IsCountExhausted 取件次数是否已耗尽(ExpiredCount 语义:-1=无限, 0=已耗尽, >0=剩余)。
与 IsExpired(时间)正交:次数耗尽 ⇒ 不能再发起**新的取件**, 但已扣次的那次取件(同一取件会话内)仍应可完成交付。
func (*FileCode) IsExpired ¶
IsExpired 检查是否**时间**过期。
⚠️ 2026-10-24 起本方法**不再**把「取件次数耗尽」算作过期: 此前 ExpiredCount==0 ⇒ IsExpired()==true ⇒ 两条通道对同一条分享给出相反结论:
- 分享页通道(select → download):扣次发生在**下载请求内**,末次取件能拿到文件;
- 取件码通道(retrieve → download):retrieve 先把次数扣到 0 ⇒ 随后的下载被判 「已过期」⇒ **末次取件必然 404,文件永远拿不到**(2026-10-24 审计)。
「次数耗尽」有自己的判定(IsCountExhausted)与错误码(10003 / 20007), 不再伪装成「不存在/已过期」。
func (*FileCode) IsPickupAvailable ¶ added in v0.32.0
IsPickupAvailable 是否还能发起新的取件(时间未过期 且 次数未耗尽)。
func (*FileCode) IsTextShare ¶
IsTextShare 判定是否纯文本分享:Text 非空且无文件路径。
回归要点(P0):文件分享会把原始文件名存进 Text 字段,因此仅凭 Text != "" 判定会把文件分享误判为文本——文件下载曾被文本分支拦截, 返回文件名字符串而非文件内容。判定逻辑此前在 app/share 包级函数, 多域(anonymous/mcp/presign)需要时各自 import share——下沉到 model 作为领域事实,消费方零跨域依赖。
type FileCodeFile ¶
type FileCodeFile struct {
gorm.Model
FileCodeID uint `gorm:"index;not null" json:"file_code_id"`
FileName string `gorm:"size:255" json:"file_name"` // 原始文件名(消毒后,仅展示)
UUIDFileName string `gorm:"size:255" json:"uuid_file_name"` // 磁盘/对象唯一名
FilePath string `gorm:"size:255" json:"file_path"` // 存储相对路径(含唯一文件名)
Size int64 `gorm:"default:0" json:"size"`
FileHash string `gorm:"size:64" json:"file_hash"` // SHA-256(秒传/完整性校验)
SortOrder int `gorm:"default:0" json:"sort_order"`
}
FileCodeFile 多文件分享的子文件行(P0 多文件:1 个 FileCode 分享 ↔ N 个文件)。
与主表的分工(2026-10-26 订正:旧注释说的是已退役的「目录+UUID 文件名」 双形态——该形态已统一为「完整相对路径」):
· 主表保留首个文件的字段(FilePath/UUIDFileName/Size/FileHash), 供管理端列表/秒传/统计读取;主表 Size = 各文件之和。 · **子表是文件清单的唯一真相源**(取件页展示/逐个下载/zip 打包都读它), 每文件一行,FilePath 同样是「含唯一文件名的完整相对路径」。
func (*FileCodeFile) DisplayName ¶
func (f *FileCodeFile) DisplayName() string
DisplayName 展示名:优先 FileName,回退 UUID 文件名/路径末段。
func (*FileCodeFile) GetFullPath ¶
func (f *FileCodeFile) GetFullPath() string
GetFullPath 取存储相对路径(与 FileCode.GetFilePath 不同:子行恒为新格式, 直接返回 FilePath)。
type FileCodeQuery ¶
type FileCodeQuery struct {
Keyword string // 模糊匹配 code/pickup_code/uuid_file_name/text/file_path
UserID *uint // 上传者
UploadType string // anonymous/authenticated/presign_*
OwnerIP string // 上传者 IP(滥用定位)
Status string // normal/blocked/pending_review
MinSize *int64 // 大小区间(字节)
MaxSize *int64
CreatedAfter *time.Time // 创建时间区间
CreatedBefore *time.Time
Expired *bool // true=仅过期(时间或次数),false=仅未过期
// Health 健康洞察过滤(2026-10-07):active/expired/expiring_soon/never_picked/forever,
// 空串=不过滤;口径见 dao.applyHealthFilter(与 CountByHealth 同源)
Health string
// Deleted 软删筛选:""=默认仅存活 / "only"=仅回收站(已软删) / "all"=全部。
// 回收站视图用 "only";配合 status/keyword 等既有筛选叠加。
Deleted string // "", "only", "all"
Page int
PageSize int
}
FileCodeQuery 管理端文件列表过滤条件(DAO ListWithFilter 消费)。 全字段可选,零值 = 不过滤。
type FileCodeStats ¶
type FileCodeStats struct {
gorm.Model
TotalFiles int64 `json:"total_files"`
TotalSize int64 `json:"total_size"`
TodayUploads int64 `json:"today_uploads"`
TodayDownloads int64 `json:"today_downloads"`
ExpiredFiles int64 `json:"expired_files"`
AnonymousFiles int64 `json:"anonymous_files"`
UserFiles int64 `json:"user_files"`
}
FileCodeStats 文件统计查询结果
type FileCodeUpdate ¶
type FileCodeUpdate struct {
gorm.Model
ExpiredAt *time.Time `json:"expired_at"`
ExpiredCount *int `json:"expired_count"`
UsedCount *int `json:"used_count"`
RequireAuth *bool `json:"require_auth"`
OwnerIP *string `json:"owner_ip"`
}
FileCodeUpdate 文件代码更新数据
type FilePreview ¶
type FilePreview struct {
ID uint `gorm:"primarykey" json:"id"`
FileCodeID uint `gorm:"uniqueIndex;not null" json:"file_code_id"` // 关联的文件ID
PreviewType string `gorm:"size:20;not null" json:"preview_type"` // 预览类型
Thumbnail string `gorm:"size:255" json:"thumbnail"` // 缩略图路径
PreviewURL string `gorm:"size:255" json:"preview_url"` // 预览URL
Width int `json:"width"` // 宽度
Height int `json:"height"` // 高度
Duration int `json:"duration"` // 时长(秒)
PageCount int `json:"page_count"` // 页数
TextContent string `gorm:"type:text" json:"text_content"` // 文本内容
MimeType string `gorm:"size:50" json:"mime_type"` // MIME类型
FileSize int64 `json:"file_size"` // 文件大小
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
FilePreview 文件预览信息(扩展FileCode模型)
type FileRequest ¶
type FileRequest struct {
gorm.Model
Token string `gorm:"uniqueIndex;size:64" json:"token"` // 链接令牌(crypto/rand 32 hex)
Title string `gorm:"size:255" json:"title"` // 投递页说明(访客可见)
UserID uint `gorm:"index" json:"user_id"` // 收件人(链接创建者)
MaxFiles int `gorm:"default:0" json:"max_files"` // 单次投递文件数上限(0=不限)
MaxBytes int64 `gorm:"default:0" json:"max_bytes"` // 单次投递总大小上限字节(0=不限)
ExpiredAt *time.Time `json:"expired_at"` // 链接过期时间(nil=永久)
UsedCount int `gorm:"default:0" json:"used_count"` // 累计投递次数
RecvBytes int64 `gorm:"default:0" json:"recv_bytes"` // 累计接收字节
}
FileRequest 寄件码/反向收件链接(P2:访客凭链接向指定用户投递文件)。 类似 Gokapi File Requests:用户创建一个投递链接(token),访客打开链接 上传文件;每个投递成为一个归属于链接主人的普通分享(复用分享的 过期/清理/通知机制),主人收到站内信+邮件通知。
type Notify ¶
type Notify struct {
gorm.Model
Title string `gorm:"size:255;not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Type string `gorm:"size:20;default:'system';index" json:"type"` // system/feature/maintenance/share_retrieved
Level string `gorm:"size:20;default:'info';index" json:"level"` // info/warning/error/success
Status int `gorm:"default:1;index" json:"status"` // 0=草稿 1=发布 2=下线
StartAt *time.Time `json:"start_at,omitempty"` // 生效时间
EndAt *time.Time `json:"end_at,omitempty"` // 失效时间
AuthorID uint `gorm:"index" json:"author_id"` // 发布管理员
TargetUserID *uint `gorm:"index" json:"target_user_id,omitempty"` // 0=广播, >0=定向给某用户
ReadAt *time.Time `gorm:"index" json:"read_at,omitempty"` // 用户首次已读时间
}
Notify 通知公告
type PickupCharge ¶ added in v0.32.0
type PickupCharge struct {
gorm.Model
// Code 规范分享码(8 位 file_codes.code;取件码入口解析后写入)。
Code string `gorm:"size:255;not null;uniqueIndex:idx_pickup_charge_code_token" json:"code"`
// TokenHash 令牌指纹(令牌签名段,hex)。
TokenHash string `gorm:"size:64;not null;uniqueIndex:idx_pickup_charge_code_token" json:"token_hash"`
// Channel 计次来源通道(select / retrieve / download),仅供观测。
Channel string `gorm:"size:32" json:"channel"`
// IP 取件人 IP(可信代理解析后)——owner 侧「取件历史」逐条展示用。
// 落的是明文 IP(与 transfer_logs 一致);若日后要脱敏,这里是唯一改动点。
IP string `gorm:"size:64" json:"ip"`
// ChargedAt 计次时刻(= 计次成功时间)。
ChargedAt time.Time `gorm:"index" json:"charged_at"`
}
PickupCharge 取件计次记录:**一个下载令牌(取件会话)至多扣 1 次**。
为什么需要这张表(2026-10-24): 「取件次数」的语义是「允许被取走多少次」,不是「允许发多少个 HTTP 请求」。 此前扣次点在**每次下载请求**上 ⇒ 三类正常行为都会重复吃次数:
- 多文件分享逐文件下载(前端每个文件一个请求)⇒ 取件次数=1 的多文件分享 只能下载**第一个**文件(第二个 403「取件次数已用完」),而「打包下载」 同一个 zip 只扣 1 次 ⇒ 两条 UI 路径的次数语义不可比;
- 断点续传 / 下载器重试(Range 请求各自是一次完整鉴权)⇒ 一次逻辑下载 吃 N 次次数;
- 收件人重复点击下载 ⇒ 同样重复吃次数。
计次键 = **下载令牌的指纹**(令牌即「一次取件」的凭证): 同一令牌 = 同一次取件会话 ⇒ 只扣 1 次;不同令牌(重新取件)= 不同取件人/次 ⇒ 各自扣 1 次。指纹用令牌签名段(HMAC 输出,不可伪造、不泄露口令)。
真相源是 DB(与项目纪律一致:不依赖 Redis ⇒ 单机内存模式与多副本部署行为一致)。
func (PickupCharge) TableName ¶ added in v0.32.0
func (PickupCharge) TableName() string
TableName 固定表名。
type SystemConfigRecord ¶
SystemConfigRecord 系统配置持久化记录(管理后台在线修改的站点配置)。 单行存储(id=1),Data 为 admin.SystemConfig 的 JSON 序列化——不走通用 KV, 也不回写 config.yaml(容器/K8s 场景 config 文件可能只读,DB 是所有部署 形态下唯一必可写存储)。
func (SystemConfigRecord) TableName ¶
func (SystemConfigRecord) TableName() string
TableName 单表语义:固定表名。
type TransferLog ¶
type TransferLog struct {
gorm.Model
Operation string `gorm:"size:20;index" json:"operation"`
FileCodeID uint `gorm:"index" json:"file_code_id"`
FileCode string `gorm:"size:255" json:"file_code"`
FileName string `gorm:"size:255" json:"file_name"`
FileSize int64 `json:"file_size"`
UserID *uint `gorm:"index" json:"user_id"`
// APIKeyID 认证所用用户级 API Key(Key 粒度归因;JWT/匿名上传为 nil)
APIKeyID *uint `gorm:"index" json:"api_key_id"`
Username string `gorm:"size:100" json:"username"`
IP string `gorm:"size:45" json:"ip"`
DurationMs int64 `json:"duration_ms"`
}
TransferLog 记录上传/下载操作日志 当系统要求登录上传/下载时,用于追踪用户行为 Operation: upload 或 download DurationMs: 针对下载记录耗时,上传默认为0 Username保留冗余信息,便于查询,即使用户被删除仍保留原始名字
type TransferLogQuery ¶
type TransferLogQuery struct {
Operation string `json:"operation"`
UserID *uint `json:"user_id"`
Search string `json:"search"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
TransferLogQuery 查询条件
type UploadChunk ¶
type UploadChunk struct {
gorm.Model
// UploadID 与 ChunkIndex 组成**复合唯一键**(2026-10-25 审计 A3):
// 无此约束时,分片失败后重传同索引会**再建一行**(dao.GetChunkByIndex 只查
// completed=true 的行 ⇒ 看不到未完成的旧行)⇒ CountCompletedChunks 按行数计
// ⇒ 「分片齐全」判定可虚高(声明 5 片只传 4 片也可能计成 5)。
//
// ⚠️ **两个字段必须挂同一个索引名**才是复合索引:只给 ChunkIndex 挂
// `uniqueIndex:` ⇒ gorm 建的是 **chunk_index 单列**唯一索引 ⇒ **不同上传
// 会话的同索引分片互相冲突**(CI 真机冒烟实测:S13e 同用户秒传
// 「UNIQUE constraint failed: upload_chunks.chunk_index」⇒ 整条秒传链路挂)。
//
// 生产走版本化迁移 000006(复合);此处标签让**新建库**(AutoMigrate)也有。
UploadID string `gorm:"index;size:36;uniqueIndex:idx_upload_chunks_upload_id_chunk_index" json:"upload_id"`
ChunkIndex int `gorm:"uniqueIndex:idx_upload_chunks_upload_id_chunk_index" json:"chunk_index"`
ChunkHash string `gorm:"size:64" json:"chunk_hash"`
TotalChunks int `json:"total_chunks"`
FileSize int64 `json:"file_size"`
ChunkSize int `json:"chunk_size"`
FileName string `gorm:"size:255" json:"file_name"`
// 归属追踪(治理 2026-10-03):分片通道此前不记录上传者,
// Complete 处置(滥用定位/归属校验)无从下手。控制记录(chunk_index=-1)填充。
OwnerIP string `gorm:"size:45" json:"owner_ip"`
UserID *uint `gorm:"index" json:"user_id"`
Completed bool `gorm:"default:false" json:"completed"`
RetryCount int `gorm:"default:0" json:"retry_count"` // 重试次数
LastError string `gorm:"type:text" json:"last_error"` // 最后错误信息
Status string `gorm:"size:20;default:'pending'" json:"status"` // pending, uploading, completed, failed
}
UploadChunk 上传分片模型
type User ¶
type User struct {
gorm.Model
Username string `gorm:"uniqueIndex;size:50" json:"username"`
Email string `gorm:"uniqueIndex;size:100" json:"email"`
PasswordHash string `gorm:"size:128" json:"-"` // 密码哈希,不返回给前端
Nickname string `gorm:"size:50" json:"nickname"` // 用户昵称
Avatar string `gorm:"size:255" json:"avatar"` // 头像URL
Role string `gorm:"size:20;default:'user'" json:"role"` // admin, user
Status string `gorm:"size:20;default:'active'" json:"status"` // active, inactive, banned
EmailVerified bool `gorm:"default:false" json:"email_verified"` // 邮箱是否验证
LastLoginAt *time.Time `json:"last_login_at"` // 最后登录时间
LastLoginIP string `gorm:"size:45" json:"last_login_ip"` // 最后登录IP
OidcSub string `gorm:"index;size:128" json:"-"` // OIDC subject(P2 SSO;空=非 OIDC)
// SessionEpoch 会话纪元:改密/封禁/降权时 +1,使全部已签发 JWT 即时失效
// (2026-10-05 审计 P2:此前封禁/重置密码后旧 JWT 仍有效至自然过期)。
SessionEpoch int `gorm:"default:0" json:"-"`
// 用户上传统计
TotalUploads int `gorm:"default:0" json:"total_uploads"` // 总上传次数
TotalDownloads int `gorm:"default:0" json:"total_downloads"` // 总下载次数
TotalStorage int64 `gorm:"default:0" json:"total_storage"` // 总存储大小(字节)
MaxUploadSize int64 `gorm:"default:0" json:"max_upload_size"` // 最大单次上传大小(字节),0表示使用系统默认
MaxStorageQuota int64 `gorm:"default:0" json:"max_storage_quota"` // 最大存储配额(字节),0表示无限制
}
type UserAPIKey ¶
type UserAPIKey struct {
gorm.Model
UserID uint `gorm:"index"`
Name string `gorm:"size:100"`
Prefix string `gorm:"size:16"`
KeyHash string `gorm:"size:64;uniqueIndex"`
LastUsedAt *time.Time
LastUsedIP string `gorm:"size:45"`
ExpiresAt *time.Time
// ExpiryNotifiedAt 临期通知去重标记;NULL=未通知
ExpiryNotifiedAt *time.Time
RevokedAt *time.Time
Revoked bool `gorm:"default:false"`
}
type UserQuery ¶
type UserQuery struct {
ID *uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
Status string `json:"status"`
Search string `json:"search"` // 模糊搜索用户名或邮箱
Limit int `json:"limit"`
Offset int `json:"offset"`
}
UserQuery 用户查询条件
type UserResp ¶
type UserResp struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Role string `json:"role"`
Status string `json:"status"`
EmailVerified bool `json:"email_verified"`
LastLoginAt *time.Time `json:"last_login_at"`
LastLoginIP string `json:"last_login_ip"`
TotalUploads int `json:"total_uploads"`
TotalDownloads int `json:"total_downloads"`
TotalStorage int64 `json:"total_storage"`
MaxUploadSize int64 `json:"max_upload_size"`
MaxStorageQuota int64 `json:"max_storage_quota"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}