feat: 多租户账号体系 + 前台收藏按钮
- 新增 users 表(user_id 数据隔离,bcrypt 密码) - 认证: 注册/登录(用户名+密码)/会话绑定用户, 首个用户成为管理员并接管旧数据 - 数据隔离: 笔记/分类/标签/回收站/版本/草稿/图谱/FTS 全部按用户隔离 - 前台: 登录/注册弹窗, 登录后★收藏自己的笔记, 游客只读公开笔记 - 后台: 用户名+密码登录, 每人管理自己的工作区, 越权访问返回404 - 冒烟测试重构+新增多租户隔离用例(78/78)
This commit is contained in:
+155
-94
@@ -22,14 +22,18 @@ type NoteService struct {
|
||||
pageSize int
|
||||
}
|
||||
|
||||
// ErrNotFound 记录不存在(映射为 HTTP 404)
|
||||
var ErrNotFound = errors.New("笔记不存在")
|
||||
|
||||
// NewNoteService 创建服务实例
|
||||
func NewNoteService(repo *repository.NoteRepository, pageSize int) *NoteService {
|
||||
return &NoteService{repo: repo, pageSize: pageSize}
|
||||
}
|
||||
|
||||
// CreateNote 创建笔记或目录
|
||||
func (s *NoteService) CreateNote(req model.NoteCreateRequest) (*model.Note, error) {
|
||||
// CreateNote 创建笔记或目录(归属当前用户)
|
||||
func (s *NoteService) CreateNote(userID uint, req model.NoteCreateRequest) (*model.Note, error) {
|
||||
note := &model.Note{
|
||||
UserID: userID,
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Category: req.Category,
|
||||
@@ -62,9 +66,9 @@ func (s *NoteService) CreateNote(req model.NoteCreateRequest) (*model.Note, erro
|
||||
return note, nil
|
||||
}
|
||||
|
||||
// GetNote 获取单条笔记(后台管理使用,任意笔记)
|
||||
func (s *NoteService) GetNote(id uint) (*model.Note, error) {
|
||||
note, err := s.repo.GetByID(id)
|
||||
// GetNote 获取单条笔记(后台/个人管理使用,校验归属)
|
||||
func (s *NoteService) GetNote(userID, id uint) (*model.Note, error) {
|
||||
note, err := s.repo.GetByIDScoped(userID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("笔记不存在")
|
||||
@@ -74,6 +78,18 @@ func (s *NoteService) GetNote(id uint) (*model.Note, error) {
|
||||
return note, nil
|
||||
}
|
||||
|
||||
// GetNotePublic 获取公开可见的笔记(游客只读公开且无密码的,用于前台)
|
||||
func (s *NoteService) GetNotePublic(id uint) (*model.Note, error) {
|
||||
note, err := s.repo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("笔记不存在")
|
||||
}
|
||||
if !note.IsPublic || note.Password != "" {
|
||||
return nil, errors.New("该笔记受保护,无法直接访问")
|
||||
}
|
||||
return note, nil
|
||||
}
|
||||
|
||||
// GetNoteContent 获取笔记内容(需密码验证,用于前台展示)
|
||||
// 返回 (note, 是否需要升级密码哈希, err)
|
||||
func (s *NoteService) GetNoteContent(id uint, password string) (*model.Note, bool, error) {
|
||||
@@ -92,11 +108,11 @@ func (s *NoteService) GetNoteContent(id uint, password string) (*model.Note, boo
|
||||
return note, false, nil
|
||||
}
|
||||
|
||||
// UpdateNote 更新笔记或目录(保存更新前快照到版本历史)
|
||||
func (s *NoteService) UpdateNote(id uint, req model.NoteUpdateRequest) (*model.Note, error) {
|
||||
note, err := s.repo.GetByID(id)
|
||||
// UpdateNote 更新笔记或目录(保存更新前快照到版本历史,校验归属)
|
||||
func (s *NoteService) UpdateNote(userID, id uint, req model.NoteUpdateRequest) (*model.Note, error) {
|
||||
note, err := s.repo.GetByIDScoped(userID, id)
|
||||
if err != nil {
|
||||
return nil, errors.New("笔记不存在")
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// 记录旧状态,判断是否产生实质内容变化
|
||||
@@ -150,11 +166,11 @@ func (s *NoteService) UpdateNote(id uint, req model.NoteUpdateRequest) (*model.N
|
||||
return note, nil
|
||||
}
|
||||
|
||||
// DeleteNote 软删除笔记或目录(目录会软删除所有子项)
|
||||
func (s *NoteService) DeleteNote(id uint) error {
|
||||
note, err := s.repo.GetByID(id)
|
||||
// DeleteNote 软删除笔记或目录(目录会软删除所有子项,校验归属)
|
||||
func (s *NoteService) DeleteNote(userID, id uint) error {
|
||||
note, err := s.repo.GetByIDScoped(userID, id)
|
||||
if err != nil {
|
||||
return errors.New("笔记不存在")
|
||||
return ErrNotFound
|
||||
}
|
||||
if note.IsFolder {
|
||||
return s.repo.DeleteWithChildren(id)
|
||||
@@ -162,18 +178,18 @@ func (s *NoteService) DeleteNote(id uint) error {
|
||||
return s.repo.Delete(id)
|
||||
}
|
||||
|
||||
// GetAllTree 获取所有笔记和目录的树形结构(管理后台用)
|
||||
func (s *NoteService) GetAllTree() ([]model.NoteListItem, error) {
|
||||
return s.repo.GetAllTree()
|
||||
// GetAllTree 获取所有笔记和目录的树形结构(个人管理用)
|
||||
func (s *NoteService) GetAllTree(userID uint) ([]model.NoteListItem, error) {
|
||||
return s.repo.GetAllTree(userID)
|
||||
}
|
||||
|
||||
// GetPublicTree 获取公开笔记的树形结构(前台用)
|
||||
func (s *NoteService) GetPublicTree() ([]model.NoteListItem, error) {
|
||||
return s.repo.GetPublicTree()
|
||||
// GetPublicTree 获取树形结构(登录用户看自己全部;游客看所有公开笔记)
|
||||
func (s *NoteService) GetPublicTree(userID uint, loggedIn bool) ([]model.NoteListItem, error) {
|
||||
return s.repo.GetPublicTree(userID, loggedIn)
|
||||
}
|
||||
|
||||
// ListNotes 获取笔记列表
|
||||
func (s *NoteService) ListNotes(pageStr, pageSizeStr, category, tag string, pinned, favorite *bool) ([]model.NoteListItem, int64, int, error) {
|
||||
// ListNotes 获取笔记列表(登录用户看自己的;游客看公开笔记)
|
||||
func (s *NoteService) ListNotes(userID uint, loggedIn bool, pageStr, pageSizeStr, category, tag string, pinned, favorite *bool) ([]model.NoteListItem, int64, int, error) {
|
||||
page := parseInt(pageStr, 1)
|
||||
pageSize := parseInt(pageSizeStr, s.pageSize)
|
||||
|
||||
@@ -185,6 +201,8 @@ func (s *NoteService) ListNotes(pageStr, pageSizeStr, category, tag string, pinn
|
||||
}
|
||||
|
||||
items, total, err := s.repo.List(repository.ListQuery{
|
||||
UserID: userID,
|
||||
LoggedIn: loggedIn,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Category: category,
|
||||
@@ -204,13 +222,13 @@ func (s *NoteService) ListNotes(pageStr, pageSizeStr, category, tag string, pinn
|
||||
return items, total, totalPages, nil
|
||||
}
|
||||
|
||||
// GetByParentID 获取指定目录下的所有项目
|
||||
func (s *NoteService) GetByParentID(parentID uint) ([]model.NoteListItem, error) {
|
||||
return s.repo.GetByParentID(parentID)
|
||||
// GetByParentID 获取指定目录下的所有项目(个人管理用)
|
||||
func (s *NoteService) GetByParentID(userID, parentID uint) ([]model.NoteListItem, error) {
|
||||
return s.repo.GetByParentID(userID, parentID)
|
||||
}
|
||||
|
||||
// SearchNotes 搜索笔记(优先使用 FTS5 全文索引,含中文分词)
|
||||
func (s *NoteService) SearchNotes(keyword, pageStr, pageSizeStr string) ([]model.NoteListItem, int64, int, error) {
|
||||
// SearchNotes 搜索笔记(优先使用 FTS5 全文索引,含中文分词;按用户隔离)
|
||||
func (s *NoteService) SearchNotes(userID uint, loggedIn bool, keyword, pageStr, pageSizeStr string) ([]model.NoteListItem, int64, int, error) {
|
||||
if keyword == "" {
|
||||
return nil, 0, 0, errors.New("搜索关键词不能为空")
|
||||
}
|
||||
@@ -227,20 +245,31 @@ func (s *NoteService) SearchNotes(keyword, pageStr, pageSizeStr string) ([]model
|
||||
// FTS5 的 MATCH 语法:对用户输入做基本转义,避免语法错误
|
||||
keyword = sanitizeFTS5(keyword)
|
||||
|
||||
items, total, err := s.repo.FTS5Search(keyword, page, pageSize)
|
||||
if err != nil {
|
||||
// FTS5 失败时回退到传统 LIKE 搜索
|
||||
items, total, err = s.repo.Search(keyword, page, pageSize)
|
||||
if loggedIn {
|
||||
items, total, err := s.repo.FTS5Search(userID, keyword, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("搜索笔记失败: %w", err)
|
||||
// FTS5 失败时回退到传统 LIKE 搜索
|
||||
items, total, err = s.repo.Search(userID, keyword, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("搜索笔记失败: %w", err)
|
||||
}
|
||||
}
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
return items, total, totalPages, nil
|
||||
}
|
||||
|
||||
// 游客:仅搜索公开笔记
|
||||
items, total, err := s.repo.SearchPublic(keyword, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("搜索笔记失败: %w", err)
|
||||
}
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return items, total, totalPages, nil
|
||||
}
|
||||
|
||||
@@ -259,41 +288,54 @@ func sanitizeFTS5(q string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// GetCategories 获取所有分类
|
||||
func (s *NoteService) GetCategories() ([]string, error) {
|
||||
return s.repo.GetCategories()
|
||||
// GetCategories 获取所有分类(登录用户看自己的;游客看公开笔记的分类)
|
||||
func (s *NoteService) GetCategories(userID uint, loggedIn bool) ([]string, error) {
|
||||
if loggedIn {
|
||||
return s.repo.GetCategories(userID)
|
||||
}
|
||||
// 游客:从公开笔记中提取分类
|
||||
return s.repo.GetPublicCategories()
|
||||
}
|
||||
|
||||
// GetTags 获取所有标签
|
||||
func (s *NoteService) GetTags() ([]string, error) {
|
||||
return s.repo.GetTags()
|
||||
// GetTags 获取所有标签(登录用户看自己的;游客看公开笔记的标签)
|
||||
func (s *NoteService) GetTags(userID uint, loggedIn bool) ([]string, error) {
|
||||
if loggedIn {
|
||||
return s.repo.GetTags(userID)
|
||||
}
|
||||
return s.repo.GetPublicTags()
|
||||
}
|
||||
|
||||
// ─────────────── 回收站 ───────────────
|
||||
|
||||
// ListTrash 获取回收站列表
|
||||
func (s *NoteService) ListTrash() ([]model.NoteListItem, error) {
|
||||
return s.repo.ListTrash()
|
||||
// ListTrash 获取回收站列表(当前用户)
|
||||
func (s *NoteService) ListTrash(userID uint) ([]model.NoteListItem, error) {
|
||||
return s.repo.ListTrash(userID)
|
||||
}
|
||||
|
||||
// RestoreNote 从回收站恢复笔记或目录(整棵子树)
|
||||
func (s *NoteService) RestoreNote(id uint) error {
|
||||
// RestoreNote 从回收站恢复笔记或目录(整棵子树,校验归属)
|
||||
func (s *NoteService) RestoreNote(userID, id uint) error {
|
||||
note, err := s.repo.GetByIDIncludingDeleted(id)
|
||||
if err != nil {
|
||||
return errors.New("记录不存在")
|
||||
}
|
||||
if note.UserID != userID {
|
||||
return errors.New("无权操作该记录")
|
||||
}
|
||||
if note.IsFolder {
|
||||
return s.repo.RestoreSubtree(id)
|
||||
}
|
||||
return s.repo.Restore(id)
|
||||
}
|
||||
|
||||
// PurgeNote 彻底删除笔记或目录(不可恢复)
|
||||
func (s *NoteService) PurgeNote(id uint) error {
|
||||
// PurgeNote 彻底删除笔记或目录(不可恢复,校验归属)
|
||||
func (s *NoteService) PurgeNote(userID, id uint) error {
|
||||
note, err := s.repo.GetByIDIncludingDeleted(id)
|
||||
if err != nil {
|
||||
return errors.New("记录不存在")
|
||||
}
|
||||
if note.UserID != userID {
|
||||
return errors.New("无权操作该记录")
|
||||
}
|
||||
if note.IsFolder {
|
||||
if err := s.repo.HardDeleteWithChildren(id); err != nil {
|
||||
return err
|
||||
@@ -308,14 +350,14 @@ func (s *NoteService) PurgeNote(id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmptyTrash 清空回收站
|
||||
func (s *NoteService) EmptyTrash() error {
|
||||
trash, err := s.repo.ListTrash()
|
||||
// EmptyTrash 清空回收站(当前用户)
|
||||
func (s *NoteService) EmptyTrash(userID uint) error {
|
||||
trash, err := s.repo.ListTrash(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range trash {
|
||||
if err := s.PurgeNote(item.ID); err != nil {
|
||||
if err := s.PurgeNote(userID, item.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -324,21 +366,27 @@ func (s *NoteService) EmptyTrash() error {
|
||||
|
||||
// ─────────────── 版本历史 ───────────────
|
||||
|
||||
// ListVersions 获取笔记版本列表
|
||||
func (s *NoteService) ListVersions(noteID uint) ([]model.NoteVersion, error) {
|
||||
// ListVersions 获取笔记版本列表(校验归属)
|
||||
func (s *NoteService) ListVersions(userID, noteID uint) ([]model.NoteVersion, error) {
|
||||
note, err := s.repo.GetByIDScoped(userID, noteID)
|
||||
if err != nil {
|
||||
return nil, errors.New("笔记不存在")
|
||||
}
|
||||
_ = note
|
||||
return s.repo.ListVersions(noteID)
|
||||
}
|
||||
|
||||
// RestoreVersion 将笔记恢复到指定版本
|
||||
func (s *NoteService) RestoreVersion(noteID, versionID uint) (*model.Note, error) {
|
||||
// RestoreVersion 将笔记恢复到指定版本(校验归属)
|
||||
func (s *NoteService) RestoreVersion(userID, noteID, versionID uint) (*model.Note, error) {
|
||||
note, err := s.repo.GetByIDScoped(userID, noteID)
|
||||
if err != nil {
|
||||
return nil, errors.New("笔记不存在")
|
||||
}
|
||||
version, err := s.repo.GetVersion(versionID)
|
||||
if err != nil {
|
||||
return nil, errors.New("版本不存在")
|
||||
}
|
||||
note, err := s.repo.GetByID(noteID)
|
||||
if err != nil {
|
||||
return nil, errors.New("笔记不存在")
|
||||
}
|
||||
_ = note
|
||||
// 保存当前状态为历史版本(防止覆盖)
|
||||
_, _ = s.repo.SaveVersion(note)
|
||||
// 回滚
|
||||
@@ -354,9 +402,9 @@ func (s *NoteService) RestoreVersion(noteID, versionID uint) (*model.Note, error
|
||||
|
||||
// ─────────────── 分享 ───────────────
|
||||
|
||||
// CreateShare 创建/更新分享令牌
|
||||
func (s *NoteService) CreateShare(noteID uint, expireHours int) (*model.Note, error) {
|
||||
note, err := s.repo.GetByID(noteID)
|
||||
// CreateShare 创建/更新分享令牌(校验归属)
|
||||
func (s *NoteService) CreateShare(userID, noteID uint, expireHours int) (*model.Note, error) {
|
||||
note, err := s.repo.GetByIDScoped(userID, noteID)
|
||||
if err != nil {
|
||||
return nil, errors.New("笔记不存在")
|
||||
}
|
||||
@@ -376,9 +424,9 @@ func (s *NoteService) CreateShare(noteID uint, expireHours int) (*model.Note, er
|
||||
return note, nil
|
||||
}
|
||||
|
||||
// RevokeShare 撤销分享
|
||||
func (s *NoteService) RevokeShare(noteID uint) error {
|
||||
note, err := s.repo.GetByID(noteID)
|
||||
// RevokeShare 撤销分享(校验归属)
|
||||
func (s *NoteService) RevokeShare(userID, noteID uint) error {
|
||||
note, err := s.repo.GetByIDScoped(userID, noteID)
|
||||
if err != nil {
|
||||
return errors.New("笔记不存在")
|
||||
}
|
||||
@@ -413,23 +461,27 @@ func (s *NoteService) UpgradePasswordHash(id uint, password string) error {
|
||||
|
||||
// ─────────────── 自动保存草稿 ───────────────
|
||||
|
||||
// SaveDraft 保存笔记草稿(仅更新草稿字段,不触发版本历史)
|
||||
// SaveDraft 保存笔记草稿(仅更新草稿字段,不触发版本历史,校验归属)
|
||||
// 返回是否有未保存草稿被记录
|
||||
func (s *NoteService) SaveDraft(id uint, content string) error {
|
||||
note, err := s.repo.GetByID(id)
|
||||
func (s *NoteService) SaveDraft(userID, id uint, content string) error {
|
||||
note, err := s.repo.GetByIDScoped(userID, id)
|
||||
if err != nil {
|
||||
return errors.New("笔记不存在")
|
||||
}
|
||||
if note.IsFolder {
|
||||
return errors.New("目录不支持草稿")
|
||||
}
|
||||
note.DraftContent = content
|
||||
_ = note
|
||||
// 直接更新草稿字段,保持 updated_at 不变(避免与正文保存混淆)
|
||||
return s.repo.UpdateFields(id, map[string]interface{}{"draft_content": content})
|
||||
}
|
||||
|
||||
// ClearDraft 清除笔记草稿(保存正文成功后调用)
|
||||
func (s *NoteService) ClearDraft(id uint) error {
|
||||
// ClearDraft 清除笔记草稿(保存正文成功后调用,校验归属)
|
||||
func (s *NoteService) ClearDraft(userID, id uint) error {
|
||||
_, err := s.repo.GetByIDScoped(userID, id)
|
||||
if err != nil {
|
||||
return errors.New("笔记不存在")
|
||||
}
|
||||
return s.repo.UpdateFields(id, map[string]interface{}{"draft_content": ""})
|
||||
}
|
||||
|
||||
@@ -438,34 +490,43 @@ func (s *NoteService) ClearDraft(id uint) error {
|
||||
// wikiLinkRe 匹配笔记正文中的 [[wiki链接]] 语法
|
||||
var wikiLinkRe = regexp.MustCompile(`\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]`)
|
||||
|
||||
// GetBacklinks 获取指向指定笔记的所有笔记(反向链接)
|
||||
func (s *NoteService) GetBacklinks(noteID uint, title string) ([]model.NoteListItem, error) {
|
||||
// GetBacklinks 获取指向指定笔记的所有笔记(反向链接,校验归属)
|
||||
func (s *NoteService) GetBacklinks(userID, noteID uint, title string) ([]model.NoteListItem, error) {
|
||||
var n *model.Note
|
||||
if title == "" {
|
||||
// 若未提供标题,先查一下
|
||||
n, err := s.repo.GetByID(noteID)
|
||||
var err error
|
||||
n, err = s.repo.GetByIDScoped(userID, noteID)
|
||||
if err != nil {
|
||||
return nil, errors.New("笔记不存在")
|
||||
}
|
||||
title = n.Title
|
||||
} else {
|
||||
// 校验归属(避免越权读取他人笔记反链)
|
||||
scoped, err := s.repo.GetByIDScoped(userID, noteID)
|
||||
if err != nil {
|
||||
return nil, errors.New("笔记不存在")
|
||||
}
|
||||
n = scoped
|
||||
}
|
||||
title = n.Title
|
||||
|
||||
all, err := s.repo.GetAllNotesLight()
|
||||
all, err := s.repo.GetAllNotesLight(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 找出所有包含 [[title]] 链接的笔记 ID
|
||||
var result []model.NoteListItem
|
||||
for _, n := range all {
|
||||
if n.ID == noteID {
|
||||
for _, note := range all {
|
||||
if note.ID == noteID {
|
||||
continue
|
||||
}
|
||||
full, err := s.repo.GetByID(n.ID)
|
||||
full, err := s.repo.GetByIDScoped(userID, note.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if wikiLinkRe.MatchString(full.Content) && strings.Contains(full.Content, "[["+title+"]]") {
|
||||
result = append(result, n)
|
||||
if strings.Contains(full.Content, "[["+title+"]]") {
|
||||
result = append(result, note)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
@@ -483,9 +544,9 @@ type GraphEdge struct {
|
||||
Target uint `json:"target"`
|
||||
}
|
||||
|
||||
// GetKnowledgeGraph 构建完整知识图谱(节点 + [[链接]] 边)
|
||||
func (s *NoteService) GetKnowledgeGraph() (map[string]interface{}, error) {
|
||||
all, err := s.repo.GetAllContentLight()
|
||||
// GetKnowledgeGraph 构建当前用户的知识图谱(节点 + [[链接]] 边)
|
||||
func (s *NoteService) GetKnowledgeGraph(userID uint) (map[string]interface{}, error) {
|
||||
all, err := s.repo.GetAllContentLight(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -526,41 +587,41 @@ func (s *NoteService) GetKnowledgeGraph() (map[string]interface{}, error) {
|
||||
|
||||
// ─────────────── 标签管理 ───────────────
|
||||
|
||||
// RenameTag 重命名标签(所有含该标签的笔记同步更新)
|
||||
func (s *NoteService) RenameTag(oldTag, newTag string) (int64, error) {
|
||||
// RenameTag 重命名标签(当前用户所有含该标签的笔记同步更新)
|
||||
func (s *NoteService) RenameTag(userID uint, oldTag, newTag string) (int64, error) {
|
||||
if oldTag == "" || newTag == "" {
|
||||
return 0, errors.New("标签名不能为空")
|
||||
}
|
||||
if oldTag == newTag {
|
||||
return 0, nil
|
||||
}
|
||||
return s.repo.UpdateTagAll(oldTag, newTag)
|
||||
return s.repo.UpdateTagAll(userID, oldTag, newTag)
|
||||
}
|
||||
|
||||
// MergeTag 将 from 标签合并到 to 标签(from 消失)
|
||||
func (s *NoteService) MergeTag(from, to string) (int64, error) {
|
||||
// MergeTag 将 from 标签合并到 to 标签(current 用户,from 消失)
|
||||
func (s *NoteService) MergeTag(userID uint, from, to string) (int64, error) {
|
||||
if from == "" || to == "" {
|
||||
return 0, errors.New("标签名不能为空")
|
||||
}
|
||||
if from == to {
|
||||
return 0, nil
|
||||
}
|
||||
return s.repo.UpdateTagAll(from, to)
|
||||
return s.repo.UpdateTagAll(userID, from, to)
|
||||
}
|
||||
|
||||
// DeleteTag 删除指定标签(从所有笔记中移除)
|
||||
func (s *NoteService) DeleteTag(tag string) (int64, error) {
|
||||
// DeleteTag 删除指定标签(当前用户所有笔记中移除)
|
||||
func (s *NoteService) DeleteTag(userID uint, tag string) (int64, error) {
|
||||
if tag == "" {
|
||||
return 0, errors.New("标签名不能为空")
|
||||
}
|
||||
return s.repo.UpdateTagAll(tag, "")
|
||||
return s.repo.UpdateTagAll(userID, tag, "")
|
||||
}
|
||||
|
||||
// GetTagUsage 获取每个标签及其使用次数
|
||||
func (s *NoteService) GetTagUsage() ([]model.TagUsage, error) {
|
||||
// GetTagUsage 获取当前用户每个标签及其使用次数
|
||||
func (s *NoteService) GetTagUsage(userID uint) ([]model.TagUsage, error) {
|
||||
var result []model.TagUsage
|
||||
counts := make(map[string]int)
|
||||
all, err := s.repo.GetAllNotesLight()
|
||||
all, err := s.repo.GetAllNotesLight(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"note-manager/model"
|
||||
"note-manager/repository"
|
||||
)
|
||||
|
||||
// UserService 用户/账号业务逻辑(多租户认证)
|
||||
type UserService struct {
|
||||
userRepo *repository.UserRepository
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务
|
||||
func NewUserService(userRepo *repository.UserRepository) *UserService {
|
||||
return &UserService{userRepo: userRepo}
|
||||
}
|
||||
|
||||
// Register 注册新用户
|
||||
// 说明:第一个注册的用户自动成为 admin(拥有平台管理权限);其余为普通 user。
|
||||
// 同时把历史遗留(user_id=0)的笔记迁移给首位注册用户。
|
||||
func (s *UserService) Register(username, password, displayName string) (*model.User, error) {
|
||||
username = strings.TrimSpace(strings.ToLower(username))
|
||||
displayName = strings.TrimSpace(displayName)
|
||||
if username == "" {
|
||||
return nil, errors.New("用户名不能为空")
|
||||
}
|
||||
if len(password) < 6 {
|
||||
return nil, errors.New("密码至少 6 位")
|
||||
}
|
||||
if _, err := s.userRepo.GetByUsername(username); err == nil {
|
||||
return nil, errors.New("用户名已存在")
|
||||
}
|
||||
|
||||
count, err := s.userRepo.Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role := "user"
|
||||
if count == 0 {
|
||||
role = "admin"
|
||||
}
|
||||
if displayName == "" {
|
||||
displayName = username
|
||||
}
|
||||
|
||||
u := &model.User{
|
||||
Username: username,
|
||||
PasswordHash: model.HashPassword(password),
|
||||
DisplayName: displayName,
|
||||
Role: role,
|
||||
}
|
||||
if err := s.userRepo.Create(u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 首位用户:接管历史遗留(user_id=0)的笔记
|
||||
if role == "admin" {
|
||||
if err := s.userRepo.GetDB().Model(&model.Note{}).
|
||||
Where("user_id = ?", 0).Update("user_id", u.ID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Login 校验用户名密码,返回用户
|
||||
func (s *UserService) Login(username, password string) (*model.User, error) {
|
||||
username = strings.TrimSpace(strings.ToLower(username))
|
||||
u, err := s.userRepo.GetByUsername(username)
|
||||
if err != nil {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
ok, _ := model.CheckPassword(password, u.PasswordHash)
|
||||
if !ok {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// GetByID 获取用户信息
|
||||
func (s *UserService) GetByID(id uint) (*model.User, error) {
|
||||
return s.userRepo.GetByID(id)
|
||||
}
|
||||
Reference in New Issue
Block a user