Help us shape the future of Go! Take the 2026 Go Developer Survey

model

package
v0.32.21 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

README

repo/db/model/ - 数据库模型

此目录存放 GORM 数据库模型定义。

内容说明

  • 数据库表映射结构体
  • 字段定义和约束
  • 模型关联关系
  • 数据转换方法

示例

type User struct {
    gorm.Model
    Username string `gorm:"uniqueIndex;size:50;not null"`
    Email    string `gorm:"uniqueIndex;size:100;not null"`
    Password string `gorm:"size:255;not null"`
    Nickname string `gorm:"size:50"`
    Avatar   string `gorm:"size:255"`
    Status   int8   `gorm:"default:1"`
}

func (u *User) TableName() string {
    return "users"
}

func (u *User) ToResp() *UserResp {
    return &UserResp{
        ID:       u.ID,
        Username: u.Username,
        // ...
    }
}

注意

  • 模型应只包含数据结构定义
  • 复杂查询逻辑应放在 dao/ 中
  • HTTP 请求/响应模型应放在 gen/http/model/

Documentation

Overview

suffix.go — 扩展名推导(仪表盘「文件类型分布」用)。

⚠️ 2026-10-26:本文件源于一次兼容层退役的漏网残留——管理台统计曾直接查 **已退役的 suffix 列**(该列恒空 ⇒ widget 早就是死的;退役后变成「SQL 报错被 `_` 吞掉」⇒ 恒空且**零日志**)。⇒ 改为从**现役列**现算扩展名。

口径与 model.FileCode.DisplayName() 对齐:文件分享的**原始文件名**在 `text` (直传/分片/预签名/本地导入四个通道都写它)⇒ 扩展名 = 最后一个点之后的尾巴。

Index

Constants

View Source
const (
	StatusNormal        = "normal"
	StatusBlocked       = "blocked"
	StatusPendingReview = "pending_review"
)

分享管控状态(治理 2026-10-03)。与软删除正交:

  • normal 正常(默认;老库 AutoMigrate 补列后全部回填 normal)
  • blocked 管理员禁用(不可取件/下载,可恢复,记录留证)
  • pending_review 内容待审核(moderation 命中且策略为 pending 时置入,审核通过恢复 normal)
View Source
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

func FileExtension(name string) string

FileExtension 取文件名的扩展名(**含前导点**,如 ".pdf")。

形态约定:与被退役的 suffix 列的旧形态一致(前端 `suffix.replace(/^\./, ”)` 照常工作)⇒ 仪表盘的「文件类型分布」不用改前端。

无扩展名("README"、"archive.")⇒ 返回空串(调用方跳过)。 含路径("a/b/c.pdf")⇒ 先取末段再判扩展名。

func IsNotReady added in v0.32.0

func IsNotReady(fc *FileCode) bool

IsNotReady 判定记录是否为「未完成上传」的占位分享:既无文件路径、也非文本分享。

与匿名通道的判定式逐字一致(!IsTextShare() && GetFilePath() == "")。 判定式必须**只有一处**(分散在多域会各自漂移 ⇒ 同一份占位分享在不同端点 给出不同结论)。

func ValidShareStatus

func ValidShareStatus(s string) bool

ValidShareStatus 状态机合法取值白名单(写入口统一校验)。

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

func (f *FileCode) DisplayName() string

DisplayName 返回分享条目的展示文件名(下载 Content-Disposition、管理/用户 列表、取件元信息等所有呈现位统一从本方法取名)。

口径统一(2026-10-26 兼容层退役):Text(原始文件名)→ UUIDFileName(存储名) → 路径末段。此前本方法以 UUIDFileName 优先,而子表 FileCodeFile.DisplayName 以 FileName 优先——同一份文件在管理端列表与取件页显示两个名字(前者显示 "56781532-….txt",后者显示 "m1.txt")。Prefix+Suffix 分支是旧直传布局的 原始名拆分,库中已无任何行使用,随死列一并退役。 纯文本分享(Text=正文且无文件路径)返回空串——文本没有文件名语义,兜底由调用方定。

func (*FileCode) GetFilePath

func (f *FileCode) GetFilePath() string

GetFilePath 获取文件路径(存储相对路径,相对存储根)。

口径统一(2026-10-26 兼容层退役):file_path 恒为「含唯一文件名的完整相对路径」, 所有写入通道(直传/分片/预签名/匿名取件/多文件/本地导入)一致。此前本方法并存 两种形态——「目录 + UUIDFileName」与「file_path 直含完整路径」——靠 strings.Contains 猜走哪条分支;同一列两种语义导致「双重路径」拼装(见 anonymous.BindFilePath 的 清空兜底注释),且匿名占位记录(file_path 空、uuid_file_name 为展示名)会拼出 不存在的路径。列语义既已统一,本方法退化为单一取值。

func (*FileCode) IsBlockedShare

func (f *FileCode) IsBlockedShare() bool

IsBlockedShare 分享是否处于管控拒绝态(blocked / pending_review)

func (*FileCode) IsCountExhausted added in v0.32.0

func (f *FileCode) IsCountExhausted() bool

IsCountExhausted 取件次数是否已耗尽(ExpiredCount 语义:-1=无限, 0=已耗尽, >0=剩余)。

与 IsExpired(时间)正交:次数耗尽 ⇒ 不能再发起**新的取件**, 但已扣次的那次取件(同一取件会话内)仍应可完成交付。

func (*FileCode) IsExpired

func (f *FileCode) IsExpired() bool

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

func (f *FileCode) IsPickupAvailable() bool

IsPickupAvailable 是否还能发起新的取件(时间未过期 且 次数未耗尽)。

func (*FileCode) IsTextShare

func (f *FileCode) IsTextShare() bool

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模型)

func (FilePreview) TableName

func (FilePreview) TableName() string

TableName 指定表名

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),访客打开链接 上传文件;每个投递成为一个归属于链接主人的普通分享(复用分享的 过期/清理/通知机制),主人收到站内信+邮件通知。

func (*FileRequest) IsExpired

func (r *FileRequest) IsExpired() bool

IsExpired 链接是否已过期

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 通知公告

func (*Notify) IsActive

func (n *Notify) IsActive() bool

IsActive 当前是否活跃(在时间窗口内 + 状态发布)

func (Notify) TableName

func (Notify) TableName() string

TableName 自定义表名

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

type SystemConfigRecord struct {
	gorm.Model
	Data string `gorm:"type:text" json:"data"`
}

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表示无限制
}

func (*User) ToResp

func (u *User) ToResp() *UserResp

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"`
}

func (UserAPIKey) TableName

func (UserAPIKey) TableName() string

TableName 指定表名

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"`
}

type UserStats

type UserStats struct {
	UserID         uint  `json:"user_id"`
	TotalUploads   int   `json:"total_uploads"`
	TotalDownloads int   `json:"total_downloads"`
	TotalStorage   int64 `json:"total_storage"`
	FileCount      int   `json:"file_count"`
}

UserStats 用户统计查询结果

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL