b93c6f1e16
- FTS5中文分词搜索(需-tags=sqlite_fts5) - [[wiki链接]]反向链接+SVG知识图谱 - 自动保存草稿(不触发版本历史) - 标签重命名/合并/删除+使用统计 - 后台编辑实时预览+格式工具栏 - PWA manifest+service worker+移动端适配 - 冒烟测试扩至66例全通过
157 lines
5.7 KiB
Go
157 lines
5.7 KiB
Go
package model
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"time"
|
||
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// bcryptCost 密码哈希成本因子
|
||
const bcryptCost = 10
|
||
|
||
// HashPassword 生成密码哈希(优先使用 bcrypt,向后兼容旧的 SHA-256)
|
||
// 返回值格式:new(bcrypt)哈希以 $2 开头;旧的 SHA-256 以 64 位十六进制开头。
|
||
func HashPassword(password string) string {
|
||
if password == "" {
|
||
return ""
|
||
}
|
||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
|
||
if err != nil {
|
||
// 极端情况下 fallback 到 sha256
|
||
s := sha256.Sum256([]byte(password))
|
||
return hex.EncodeToString(s[:])
|
||
}
|
||
return string(hash)
|
||
}
|
||
|
||
// IsBcryptHash 判断哈希是否为 bcrypt 格式
|
||
func IsBcryptHash(hash string) bool {
|
||
return len(hash) >= 4 && hash[:4] == "$2a$" || (len(hash) >= 4 && hash[:4] == "$2b$") || (len(hash) >= 4 && hash[:4] == "$2y$")
|
||
}
|
||
|
||
// CheckPassword 验证密码(兼容 bcrypt 与旧 SHA-256 哈希)
|
||
// 第二个返回值表示是否应当把存储的哈希升级为 bcrypt(旧 sha256 命中时返回 true)
|
||
func CheckPassword(password, hash string) (bool, bool) {
|
||
if hash == "" {
|
||
return true, false
|
||
}
|
||
if password == "" {
|
||
return false, false
|
||
}
|
||
if IsBcryptHash(hash) {
|
||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||
return err == nil, false
|
||
}
|
||
// 旧的 SHA-256 哈希
|
||
s := sha256.Sum256([]byte(password))
|
||
if hex.EncodeToString(s[:]) == hash {
|
||
// 命中旧哈希,建议升级为 bcrypt
|
||
return true, true
|
||
}
|
||
return false, false
|
||
}
|
||
|
||
// Note 笔记模型(也用于目录)
|
||
type Note struct {
|
||
ID uint `json:"id" gorm:"primaryKey"`
|
||
Title string `json:"title" gorm:"size:255;not null"`
|
||
Content string `json:"content" gorm:"type:text"`
|
||
DraftContent string `json:"draft_content,omitempty" gorm:"type:text"` // 未保存的草稿(自动保存用)
|
||
Category string `json:"category" gorm:"size:100;index"`
|
||
Tags string `json:"tags" gorm:"type:text"` // JSON 数组格式存储
|
||
Password string `json:"-" gorm:"size:255"` // 访问密码(哈希存储)
|
||
IsPinned bool `json:"is_pinned" gorm:"default:false"`
|
||
IsFavorite bool `json:"is_favorite" gorm:"default:false"`
|
||
IsPublic bool `json:"is_public"` // 是否公开
|
||
ParentID uint `json:"parent_id" gorm:"default:0;index"` // 父级目录 ID,0 表示根目录
|
||
IsFolder bool `json:"is_folder" gorm:"default:false"` // 是否为文件夹
|
||
SortOrder int `json:"sort_order" gorm:"default:0"` // 排序顺序
|
||
ShareToken string `json:"-" gorm:"size:64;index"` // 分享令牌,空 = 未分享
|
||
ShareExpireAt *time.Time `json:"share_expire_at,omitempty"` // 分享过期时间,nil = 永久
|
||
VisitCount int `json:"visit_count" gorm:"default:0"` // 浏览次数
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除
|
||
}
|
||
|
||
// NoteListItem 笔记列表响应(不含内容和密码)
|
||
type NoteListItem struct {
|
||
ID uint `json:"id"`
|
||
Title string `json:"title"`
|
||
Category string `json:"category"`
|
||
Tags string `json:"tags"`
|
||
HasPassword bool `json:"has_password"` // 是否有密码保护
|
||
IsPinned bool `json:"is_pinned"`
|
||
IsFavorite bool `json:"is_favorite"`
|
||
IsPublic bool `json:"is_public"`
|
||
ParentID uint `json:"parent_id"`
|
||
IsFolder bool `json:"is_folder"`
|
||
SortOrder int `json:"sort_order"`
|
||
ShareToken string `json:"share_token,omitempty"` // 分享令牌(后台完整列表需要)
|
||
ShareExpireAt *time.Time `json:"share_expire_at,omitempty"`
|
||
VisitCount int `json:"visit_count"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// NoteCreateRequest 创建笔记请求
|
||
type NoteCreateRequest struct {
|
||
Title string `json:"title" binding:"required"`
|
||
Content string `json:"content"`
|
||
Category string `json:"category"`
|
||
Tags string `json:"tags"`
|
||
Password string `json:"password"`
|
||
IsPinned *bool `json:"is_pinned"`
|
||
IsFavorite *bool `json:"is_favorite"`
|
||
IsPublic *bool `json:"is_public"`
|
||
ParentID *uint `json:"parent_id"`
|
||
IsFolder bool `json:"is_folder"`
|
||
SortOrder int `json:"sort_order"`
|
||
}
|
||
|
||
// NoteUpdateRequest 更新笔记请求
|
||
type NoteUpdateRequest struct {
|
||
Title *string `json:"title"`
|
||
Content *string `json:"content"`
|
||
Category *string `json:"category"`
|
||
Tags *string `json:"tags"`
|
||
Password *string `json:"password"`
|
||
RemovePassword *bool `json:"remove_password"` // 是否移除密码
|
||
IsPinned *bool `json:"is_pinned"`
|
||
IsFavorite *bool `json:"is_favorite"`
|
||
IsPublic *bool `json:"is_public"`
|
||
ParentID *uint `json:"parent_id"`
|
||
IsFolder *bool `json:"is_folder"`
|
||
SortOrder *int `json:"sort_order"`
|
||
}
|
||
|
||
// NoteAccessRequest 笔记访问请求(验证密码)
|
||
type NoteAccessRequest struct {
|
||
Password string `json:"password"`
|
||
}
|
||
|
||
// NoteVersion 笔记版本历史
|
||
type NoteVersion struct {
|
||
ID uint `json:"id" gorm:"primaryKey"`
|
||
NoteID uint `json:"note_id" gorm:"index"`
|
||
Title string `json:"title"`
|
||
Content string `json:"content" gorm:"type:text"`
|
||
Category string `json:"category"`
|
||
Tags string `json:"tags"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
// TableName 指定版本表名
|
||
func (NoteVersion) TableName() string {
|
||
return "note_versions"
|
||
}
|
||
|
||
// TagUsage 标签使用统计
|
||
type TagUsage struct {
|
||
Name string `json:"name"`
|
||
Count int `json:"count"`
|
||
}
|