Files
note-manager/model/note.go
T
Your Name d9793300f9 feat: 多租户账号体系 + 前台收藏按钮
- 新增 users 表(user_id 数据隔离,bcrypt 密码)
- 认证: 注册/登录(用户名+密码)/会话绑定用户, 首个用户成为管理员并接管旧数据
- 数据隔离: 笔记/分类/标签/回收站/版本/草稿/图谱/FTS 全部按用户隔离
- 前台: 登录/注册弹窗, 登录后★收藏自己的笔记, 游客只读公开笔记
- 后台: 用户名+密码登录, 每人管理自己的工作区, 越权访问返回404
- 冒烟测试重构+新增多租户隔离用例(78/78)
2026-08-11 12:58:12 +08:00

158 lines
5.8 KiB
Go
Raw 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 (
"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"`
UserID uint `json:"user_id" gorm:"index;not null;default:0"` // 所属用户(多租户隔离)
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"` // 父级目录 ID0 表示根目录
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"`
}