backend: add bookmark CRUD with public list endpoint

New bookmarks feature: domain model, repository, service and handler
supporting list/create/update/delete. Public endpoint exposes the
admin user's bookmarks for the homepage navigation grid; authenticated
endpoints scope by user. Dev Dockerfile drops air for plain `go run`
and uses goproxy.cn to avoid build failures on the deploy host.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 01:51:43 +08:00
parent efd644dc67
commit ffecc9451d
9 changed files with 343 additions and 3 deletions

View File

@@ -0,0 +1,87 @@
package service
import (
"errors"
"evanpage-backend/internal/domain"
"evanpage-backend/internal/repository"
)
type BookmarkService struct {
repo *repository.BookmarkRepository
}
func NewBookmarkService(repo *repository.BookmarkRepository) *BookmarkService {
return &BookmarkService{repo: repo}
}
func (s *BookmarkService) Create(userID uint, title, url, description, icon, category string, sortOrder int) (*domain.Bookmark, error) {
if title == "" || url == "" {
return nil, errors.New("title and url are required")
}
if category == "" {
category = "默认"
}
bm := &domain.Bookmark{
UserID: userID,
Title: title,
URL: url,
Description: description,
Icon: icon,
Category: category,
SortOrder: sortOrder,
}
if err := s.repo.Create(bm); err != nil {
return nil, err
}
return bm, nil
}
func (s *BookmarkService) ListByUser(userID uint) ([]domain.Bookmark, error) {
return s.repo.ListByUser(userID)
}
func (s *BookmarkService) ListByUserAndCategory(userID uint, category string) ([]domain.Bookmark, error) {
return s.repo.ListByUserAndCategory(userID, category)
}
func (s *BookmarkService) ListCategoriesByUser(userID uint) ([]string, error) {
return s.repo.ListCategoriesByUser(userID)
}
func (s *BookmarkService) Update(userID, bookmarkID uint, title, url, description, icon, category string, sortOrder int) (*domain.Bookmark, error) {
bm, err := s.repo.FindByID(bookmarkID)
if err != nil {
return nil, errors.New("bookmark not found")
}
if bm.UserID != userID {
return nil, errors.New("forbidden")
}
if title != "" {
bm.Title = title
}
if url != "" {
bm.URL = url
}
bm.Description = description
bm.Icon = icon
if category != "" {
bm.Category = category
}
bm.SortOrder = sortOrder
if err := s.repo.Update(bm); err != nil {
return nil, err
}
return bm, nil
}
func (s *BookmarkService) Delete(userID, bookmarkID uint) error {
bm, err := s.repo.FindByID(bookmarkID)
if err != nil {
return errors.New("bookmark not found")
}
if bm.UserID != userID {
return errors.New("forbidden")
}
return s.repo.Delete(bookmarkID)
}

View File

@@ -93,6 +93,10 @@ func (s *UserService) BindKeycloak(username, password, keycloakID, keycloakEmail
return user, nil
}
func (s *UserService) FindByRole(role string) (*domain.User, error) {
return s.repo.FindByRole(role)
}
func (s *UserService) ListUsers() ([]domain.User, error) {
return s.repo.ListAll()
}