migrate from github
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package auth_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/auth"
|
||||
"github.com/XingfenD/yoresee_doc/internal/constant"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/media"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/config_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/invitation_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
db *gorm.DB
|
||||
userRepo *user_repo.UserRepository
|
||||
}
|
||||
|
||||
func NewAuthService(repos *repository.Repositories) *AuthService {
|
||||
return &AuthService{
|
||||
db: repos.DB,
|
||||
userRepo: repos.User,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthService) Register(ctx context.Context, userCreate *dto.UserCreate) error {
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
mode := config_service.ConfigSvc.GetSystemRegisterMode(ctx)
|
||||
if mode == constant.RegisterMode_Invite {
|
||||
if userCreate.InvitationCode == nil || *userCreate.InvitationCode == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "the system enable invitation mode, but the invitation code is empty")
|
||||
}
|
||||
err := invitation_service.InvitationSvc.ValidateAndUseWithTx(tx, *userCreate.InvitationCode)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: AuthService] ValidateAndUseWithTx failed, invitation_code=%s, err=%+v", *userCreate.InvitationCode, err)
|
||||
_ = invitation_service.InvitationSvc.CreateInvitationRecord(&model.InvitationRecord{
|
||||
Code: *userCreate.InvitationCode,
|
||||
UsedBy: userCreate.Email,
|
||||
Status: "failed",
|
||||
UsedAt: time.Now(),
|
||||
})
|
||||
return status.GenErrWithCustomMsg(err, "invitation validation failed")
|
||||
}
|
||||
}
|
||||
|
||||
_, err := s.userRepo.GetByEmail(userCreate.Email).WithTx(tx).Exec()
|
||||
if err == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusUserAlreadyExists, "email already registered")
|
||||
}
|
||||
|
||||
hashedPwd, err := utils.HashPassword(userCreate.Password)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: AuthService] HashPassword failed, email=%s, err=%+v", userCreate.Email, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "hash password failed")
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
ExternalID: utils.GenerateExternalID(utils.ExternalIDContextUser),
|
||||
Username: userCreate.Username,
|
||||
PasswordHash: hashedPwd,
|
||||
Email: userCreate.Email,
|
||||
Status: 1,
|
||||
InvitationCode: userCreate.InvitationCode,
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: AuthService] Create user failed, email=%s, err=%+v", userCreate.Email, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create user failed")
|
||||
}
|
||||
|
||||
if mode == constant.RegisterMode_Invite && userCreate.InvitationCode != nil && *userCreate.InvitationCode != "" {
|
||||
_ = invitation_service.InvitationSvc.CreateInvitationRecordWithTx(tx, &model.InvitationRecord{
|
||||
Code: *userCreate.InvitationCode,
|
||||
UsedByUserID: utils.Of(user.ID),
|
||||
UsedBy: user.Email,
|
||||
Status: "success",
|
||||
UsedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(email string, password string) (string, *dto.UserResponse, error) {
|
||||
user, err := s.userRepo.GetByEmail(email).Exec()
|
||||
if err != nil {
|
||||
return "", nil, status.StatusUserNotFound
|
||||
}
|
||||
if user.Status <= 0 {
|
||||
return "", nil, status.GenErrWithCustomMsg(status.StatusPermissionDenied, "user is banned")
|
||||
}
|
||||
|
||||
if !utils.CheckPassword(password, user.PasswordHash) {
|
||||
return "", nil, status.StatusInvalidPassword
|
||||
}
|
||||
|
||||
token, err := auth.GenerateToken(user.ExternalID, user.Username)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: AuthService] GenerateToken failed, user_external_id=%s, err=%+v", user.ExternalID, err)
|
||||
return "", nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "generate token failed")
|
||||
}
|
||||
|
||||
userResponse := dto.NewUserResponseFromModel(user)
|
||||
userResponse.Avatar = media.BuildAvatarURL(userResponse.ExternalID, userResponse.AvatarObjectKey, userResponse.AvatarVersion)
|
||||
|
||||
return token, userResponse, nil
|
||||
}
|
||||
|
||||
var AuthSvc *AuthService
|
||||
@@ -0,0 +1,71 @@
|
||||
package auth_service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
const (
|
||||
SideBarSceneHome = "home"
|
||||
SideBarSceneUserInfo = "user_info"
|
||||
SideBarSceneManage = "manage"
|
||||
)
|
||||
|
||||
var (
|
||||
homeTabs = []string{
|
||||
"home",
|
||||
"search",
|
||||
"documents",
|
||||
"knowledge-base",
|
||||
"templates",
|
||||
}
|
||||
userInfoTabs = []string{
|
||||
"home",
|
||||
"user-center",
|
||||
"user-notifications",
|
||||
"user-invite",
|
||||
"user-security",
|
||||
}
|
||||
manageTabsForAdmin = []string{
|
||||
"home",
|
||||
"manage-user",
|
||||
"manage-user-group",
|
||||
"manage-organization",
|
||||
"manage-invite",
|
||||
"manage-security",
|
||||
}
|
||||
manageTabsForNormalUser = []string{
|
||||
"home",
|
||||
}
|
||||
)
|
||||
|
||||
func (s *AuthService) QuerySideBarDisplay(scene string, isAdmin bool) ([]string, error) {
|
||||
if scene == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
switch scene {
|
||||
case SideBarSceneHome:
|
||||
return homeTabs, nil
|
||||
case SideBarSceneUserInfo:
|
||||
return userInfoTabs, nil
|
||||
case SideBarSceneManage:
|
||||
if isAdmin {
|
||||
return manageTabsForAdmin, nil
|
||||
}
|
||||
return manageTabsForNormalUser, nil
|
||||
default:
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid scene")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthService) IsAdmin(userExternalID string) (bool, error) {
|
||||
if userExternalID == "" {
|
||||
return false, status.StatusParamError
|
||||
}
|
||||
user, err := s.userRepo.GetByExternalID(userExternalID).Exec()
|
||||
if err != nil || user == nil {
|
||||
return false, status.StatusUserNotFound
|
||||
}
|
||||
return strings.EqualFold(user.Username, "admin"), nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package auth_service
|
||||
|
||||
import "github.com/XingfenD/yoresee_doc/internal/status"
|
||||
|
||||
const (
|
||||
TopNavMenuUserCenter = "user-center"
|
||||
TopNavMenuSystemManage = "system-manage"
|
||||
)
|
||||
|
||||
var (
|
||||
topNavMenusForAdmin = []string{
|
||||
TopNavMenuUserCenter,
|
||||
TopNavMenuSystemManage,
|
||||
}
|
||||
topNavMenusForNormalUser = []string{
|
||||
TopNavMenuUserCenter,
|
||||
}
|
||||
)
|
||||
|
||||
func (s *AuthService) QueryTopNavDisplay(isAdmin bool) ([]string, error) {
|
||||
if isAdmin {
|
||||
return topNavMenusForAdmin, nil
|
||||
}
|
||||
if topNavMenusForNormalUser == nil {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
return topNavMenusForNormalUser, nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package auth_service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/config"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/media"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/storage"
|
||||
)
|
||||
|
||||
func (s *AuthService) UpdateProfile(userExternalID string, req *dto.UpdateProfileRequest) (*dto.UserResponse, error) {
|
||||
if strings.TrimSpace(userExternalID) == "" || req == nil {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "user_external_id and request are required")
|
||||
}
|
||||
if req.Username == nil && req.Email == nil && req.Nickname == nil && req.Password == nil && req.AvatarFile == nil {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "no profile fields to update")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByExternalID(userExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusUserNotFound, "user not found")
|
||||
}
|
||||
|
||||
if req.Username != nil {
|
||||
username := strings.TrimSpace(*req.Username)
|
||||
if username == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "username cannot be empty")
|
||||
}
|
||||
user.Username = username
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
email := strings.TrimSpace(*req.Email)
|
||||
if email == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "email cannot be empty")
|
||||
}
|
||||
existingUser, getErr := s.userRepo.GetByEmail(email).Exec()
|
||||
if getErr == nil && existingUser.ID != user.ID {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusUserAlreadyExists, "email already registered")
|
||||
}
|
||||
user.Email = email
|
||||
}
|
||||
|
||||
if req.Nickname != nil {
|
||||
user.Nickname = strings.TrimSpace(*req.Nickname)
|
||||
}
|
||||
|
||||
if req.Password != nil {
|
||||
password := strings.TrimSpace(*req.Password)
|
||||
if password == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "password cannot be empty")
|
||||
}
|
||||
hashedPwd, hashErr := utils.HashPassword(password)
|
||||
if hashErr != nil {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "hash password failed")
|
||||
}
|
||||
user.PasswordHash = hashedPwd
|
||||
}
|
||||
|
||||
var oldAvatarObjectKey string
|
||||
if req.AvatarFile != nil {
|
||||
oldAvatarObjectKey = user.AvatarObjectKey
|
||||
avatarObjectKey, nextVersion, uploadErr := uploadAvatar(
|
||||
user.ExternalID,
|
||||
user.AvatarVersion+1,
|
||||
req.AvatarFile,
|
||||
req.AvatarFilename,
|
||||
req.AvatarContentType,
|
||||
)
|
||||
if uploadErr != nil {
|
||||
return nil, status.GenErrWithCustomMsg(uploadErr, "upload avatar failed")
|
||||
}
|
||||
now := time.Now()
|
||||
user.AvatarObjectKey = avatarObjectKey
|
||||
user.AvatarVersion = nextVersion
|
||||
user.AvatarUpdatedAt = &now
|
||||
user.Avatar = ""
|
||||
}
|
||||
|
||||
if err := s.userRepo.Update(user).Exec(); err != nil {
|
||||
return nil, status.StatusWriteDBError
|
||||
}
|
||||
|
||||
if oldAvatarObjectKey != "" && oldAvatarObjectKey != user.AvatarObjectKey {
|
||||
_ = storage.DeleteFile(config.GlobalConfig.Minio.Bucket, oldAvatarObjectKey)
|
||||
}
|
||||
|
||||
resp := dto.NewUserResponseFromModel(user)
|
||||
resp.Avatar = media.BuildAvatarURL(resp.ExternalID, resp.AvatarObjectKey, resp.AvatarVersion)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func uploadAvatar(userExternalID string, avatarVersion int64, avatarFile []byte, filename, contentType *string) (string, int64, error) {
|
||||
if len(avatarFile) == 0 {
|
||||
return "", 0, status.GenErrWithCustomMsg(status.StatusParamError, "avatar file is empty")
|
||||
}
|
||||
if len(avatarFile) > media.MaxAvatarSize {
|
||||
return "", 0, status.GenErrWithCustomMsg(status.StatusParamError, "avatar file exceeds 5MB limit")
|
||||
}
|
||||
if storage.MinioClient == nil {
|
||||
return "", 0, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "minio client is not initialized")
|
||||
}
|
||||
|
||||
fileName := "avatar"
|
||||
if filename != nil {
|
||||
fileName = strings.TrimSpace(*filename)
|
||||
}
|
||||
|
||||
fileContentType := "application/octet-stream"
|
||||
if contentType != nil {
|
||||
fileContentType = strings.TrimSpace(*contentType)
|
||||
}
|
||||
fileContentType = media.NormalizeAvatarContentType(avatarFile, fileContentType)
|
||||
if !media.IsSupportedAvatarContentType(fileContentType) {
|
||||
return "", 0, status.GenErrWithCustomMsg(status.StatusParamError, "avatar content type is not supported")
|
||||
}
|
||||
|
||||
objectName := media.BuildAvatarObjectKey(userExternalID, avatarVersion, media.ResolveAvatarExt(fileName, fileContentType))
|
||||
|
||||
err := storage.PutFile(
|
||||
config.GlobalConfig.Minio.Bucket,
|
||||
objectName,
|
||||
bytes.NewReader(avatarFile),
|
||||
int64(len(avatarFile)),
|
||||
fileContentType,
|
||||
)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return objectName, avatarVersion, nil
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package comment_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/constant"
|
||||
"github.com/XingfenD/yoresee_doc/internal/domain_event"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/comment_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/document_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/knowledge_base_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CommentService struct {
|
||||
commentRepo *comment_repo.CommentRepository
|
||||
documentRepo *document_repo.DocumentRepository
|
||||
userRepo *user_repo.UserRepository
|
||||
kbRepo *knowledge_base_repo.KnowledgeBaseRepository
|
||||
}
|
||||
|
||||
func NewCommentService(repos *repository.Repositories) *CommentService {
|
||||
return &CommentService{
|
||||
commentRepo: repos.Comment,
|
||||
documentRepo: repos.Document,
|
||||
userRepo: repos.User,
|
||||
kbRepo: repos.KnowledgeBase,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CommentService) CreateComment(req *dto.CreateCommentRequest) (*model.DocumentComment, error) {
|
||||
if req == nil || strings.TrimSpace(req.DocumentExternalID) == "" || strings.TrimSpace(req.Content) == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
if strings.TrimSpace(req.CreatorExternalID) == "" {
|
||||
return nil, status.StatusTokenInvalid
|
||||
}
|
||||
doc, err := s.documentRepo.GetByExternalID(req.DocumentExternalID).Exec(context.Background())
|
||||
if err != nil || doc == nil {
|
||||
return nil, status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
creatorID, err := s.userRepo.GetIDByExternalID(req.CreatorExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusUserNotFound
|
||||
}
|
||||
|
||||
parentID := int64(0)
|
||||
var parentComment *model.DocumentComment
|
||||
if req.ParentExternalID != nil && strings.TrimSpace(*req.ParentExternalID) != "" {
|
||||
parent, err := s.commentRepo.GetByExternalID(*req.ParentExternalID).Exec()
|
||||
if err != nil || parent == nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
parentID = parent.ID
|
||||
parentComment = parent
|
||||
}
|
||||
|
||||
item := &model.DocumentComment{
|
||||
ExternalID: utils.GenerateExternalID(utils.ExternalIDContextComment),
|
||||
DocumentID: doc.ID,
|
||||
ParentID: parentID,
|
||||
CreatorID: creatorID,
|
||||
Content: req.Content,
|
||||
}
|
||||
if req.AnchorID != nil && strings.TrimSpace(*req.AnchorID) != "" {
|
||||
item.AnchorID = strings.TrimSpace(*req.AnchorID)
|
||||
} else if parentComment != nil && strings.TrimSpace(parentComment.AnchorID) != "" {
|
||||
item.AnchorID = parentComment.AnchorID
|
||||
}
|
||||
if strings.TrimSpace(item.AnchorID) == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
if err := s.commentRepo.Create(item).Exec(); err != nil {
|
||||
return nil, status.StatusWriteDBError
|
||||
}
|
||||
|
||||
notifyQuote := ""
|
||||
if req.Quote != nil {
|
||||
notifyQuote = strings.TrimSpace(*req.Quote)
|
||||
}
|
||||
s.notifyCommentTargets(doc, item, parentComment, notifyQuote, req.MentionedUserExternalIDs)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *CommentService) ListComments(req *dto.ListCommentsRequest) ([]model.DocumentComment, int64, error) {
|
||||
if req == nil || strings.TrimSpace(req.DocumentExternalID) == "" {
|
||||
return nil, 0, status.StatusParamError
|
||||
}
|
||||
doc, err := s.documentRepo.GetByExternalID(req.DocumentExternalID).Exec(context.Background())
|
||||
if err != nil || doc == nil {
|
||||
return nil, 0, status.StatusDocumentNotFound
|
||||
}
|
||||
op := s.commentRepo.ListByDocument(doc.ID).
|
||||
WithPagination(req.Page, req.PageSize)
|
||||
list, total, err := op.ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: CommentService] list comments failed, document_external_id=%s, err=%+v", req.DocumentExternalID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusReadDBError, "list comments failed")
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
func (s *CommentService) DeleteComment(req *dto.DeleteCommentRequest, isAdmin bool) error {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
comment, err := s.commentRepo.GetByExternalID(req.ExternalID).Exec()
|
||||
if err != nil || comment == nil {
|
||||
return status.StatusReadDBError
|
||||
}
|
||||
if !isAdmin {
|
||||
operatorID, err := s.userRepo.GetIDByExternalID(req.OperatorExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
if operatorID != comment.CreatorID {
|
||||
return status.StatusPermissionDenied
|
||||
}
|
||||
}
|
||||
if err := s.commentRepo.Delete(comment.ID).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CommentService) UpdateComment(req *dto.UpdateCommentRequest, isAdmin bool) (*model.DocumentComment, error) {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" || strings.TrimSpace(req.Content) == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
comment, err := s.commentRepo.GetByExternalID(req.ExternalID).Exec()
|
||||
if err != nil || comment == nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
if !isAdmin {
|
||||
operatorID, err := s.userRepo.GetIDByExternalID(req.OperatorExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusUserNotFound
|
||||
}
|
||||
if operatorID != comment.CreatorID {
|
||||
return nil, status.StatusPermissionDenied
|
||||
}
|
||||
}
|
||||
if err := s.commentRepo.UpdateContentByID(comment.ID, req.Content).Exec(); err != nil {
|
||||
return nil, status.StatusWriteDBError
|
||||
}
|
||||
comment.Content = req.Content
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
var CommentSvc *CommentService
|
||||
|
||||
func (s *CommentService) notifyCommentTargets(doc *model.Document, comment *model.DocumentComment, parent *model.DocumentComment, quote string, mentionedUserExternalIDs []string) {
|
||||
if doc == nil || comment == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve knowledge base external ID for navigation payload
|
||||
kbExternalID := ""
|
||||
if doc.KnowledgeID != nil && *doc.KnowledgeID != 0 {
|
||||
kb, err := s.kbRepo.GetByID(*doc.KnowledgeID).Exec()
|
||||
if err == nil && kb != nil {
|
||||
kbExternalID = kb.ExternalID
|
||||
}
|
||||
}
|
||||
|
||||
parentExternalID := ""
|
||||
if parent != nil && parent.ID != 0 {
|
||||
parentExternalID = parent.ExternalID
|
||||
}
|
||||
|
||||
payloadBase := map[string]any{
|
||||
"document_external_id": doc.ExternalID,
|
||||
"comment_external_id": comment.ExternalID,
|
||||
"document_title": doc.Title,
|
||||
"parent_comment_external_id": parentExternalID,
|
||||
"knowledge_base_external_id": kbExternalID,
|
||||
"mentioned_user_external_ids": mentionedUserExternalIDs,
|
||||
}
|
||||
payloadJSON, _ := json.Marshal(payloadBase)
|
||||
payloadStr := string(payloadJSON)
|
||||
|
||||
publish := func(receivers []string, notifType, title string) {
|
||||
if len(receivers) == 0 {
|
||||
return
|
||||
}
|
||||
evt := domain_event.NotificationCreateEvent{
|
||||
ReceiverExternalIDs: receivers,
|
||||
Type: notifType,
|
||||
Title: title,
|
||||
Content: comment.Content,
|
||||
PayloadJSON: payloadStr,
|
||||
}
|
||||
if err := domain_event.PublishNotificationCreateEvent(context.Background(), evt); err != nil {
|
||||
logrus.Errorf("publish %s notification failed: %v", notifType, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Build mention receiver set (highest priority — skip for other types)
|
||||
mentionSet := map[string]struct{}{}
|
||||
for _, id := range mentionedUserExternalIDs {
|
||||
id = strings.TrimSpace(id)
|
||||
if id != "" && id != comment.ExternalID {
|
||||
mentionSet[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Build reply receivers (parent comment creator, excluding commenter and mention receivers)
|
||||
var replyReceivers []string
|
||||
if parent != nil && parent.ID != 0 {
|
||||
parentCreator, err := s.userRepo.GetByID(parent.CreatorID).Exec()
|
||||
if err == nil && parentCreator != nil {
|
||||
id := strings.TrimSpace(parentCreator.ExternalID)
|
||||
if id != "" && parentCreator.ID != comment.CreatorID {
|
||||
if _, isMentioned := mentionSet[id]; !isMentioned {
|
||||
replyReceivers = append(replyReceivers, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build comment receivers (document owner, excluding commenter, reply receivers, and mention receivers)
|
||||
replySet := map[string]struct{}{}
|
||||
for _, id := range replyReceivers {
|
||||
replySet[id] = struct{}{}
|
||||
}
|
||||
var commentReceivers []string
|
||||
if doc.UserID != comment.CreatorID {
|
||||
owner, err := s.userRepo.GetByID(doc.UserID).Exec()
|
||||
if err == nil && owner != nil {
|
||||
id := strings.TrimSpace(owner.ExternalID)
|
||||
if id != "" {
|
||||
if _, isMentioned := mentionSet[id]; !isMentioned {
|
||||
if _, isReply := replySet[id]; !isReply {
|
||||
commentReceivers = append(commentReceivers, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mentionReceivers := make([]string, 0, len(mentionSet))
|
||||
for id := range mentionSet {
|
||||
mentionReceivers = append(mentionReceivers, id)
|
||||
}
|
||||
|
||||
publish(mentionReceivers, constant.NotificationType_Mention, constant.GetNotificationTitle(constant.NotificationType_Mention))
|
||||
publish(replyReceivers, constant.NotificationType_Reply, constant.GetNotificationTitle(constant.NotificationType_Reply))
|
||||
publish(commentReceivers, constant.NotificationType_Comment, constant.GetNotificationTitle(constant.NotificationType_Comment))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package config_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/storage"
|
||||
)
|
||||
|
||||
type ConfigService struct {
|
||||
SystemRegisterMode func() string `consul:"system.security.register_mode,default=invite"`
|
||||
SystemRegisterLimit func() bool `consul:"system.security.register_limit"`
|
||||
}
|
||||
|
||||
func NewConfigService() *ConfigService {
|
||||
if !storage.ConsulEnabled() {
|
||||
return nil
|
||||
}
|
||||
s := &ConfigService{}
|
||||
if err := storage.BindConsulConfig(s, storage.Consul); err != nil {
|
||||
panic("consul service init failed")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ConfigService) GetSystemRegisterMode(ctx context.Context) string {
|
||||
return s.SystemRegisterMode()
|
||||
}
|
||||
|
||||
func (s *ConfigService) GetSystemRegisterLimit(ctx context.Context) bool {
|
||||
return s.SystemRegisterLimit()
|
||||
}
|
||||
|
||||
func (s *ConfigService) Set(ctx context.Context, key, value string) error {
|
||||
if !storage.ConsulEnabled() {
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "consul is not enabled")
|
||||
}
|
||||
return storage.Consul.Set(ctx, key, value)
|
||||
}
|
||||
|
||||
var ConfigSvc *ConfigService
|
||||
|
||||
func InitConfigService() {
|
||||
ConfigSvc = NewConfigService()
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/config"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/storage"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const maxAttachmentSize = 5 * 1024 * 1024
|
||||
|
||||
func (s *DocumentService) UploadAttachment(
|
||||
ctx context.Context,
|
||||
userExternalID string,
|
||||
documentExternalID string,
|
||||
file []byte,
|
||||
fileName string,
|
||||
contentType string,
|
||||
) (*dto.AttachmentResponse, error) {
|
||||
if s == nil || strings.TrimSpace(userExternalID) == "" || strings.TrimSpace(documentExternalID) == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid upload attachment request")
|
||||
}
|
||||
if len(file) == 0 || len(file) > maxAttachmentSize {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid attachment file")
|
||||
}
|
||||
if config.GlobalConfig == nil || strings.TrimSpace(config.GlobalConfig.Minio.Bucket) == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "attachment storage is not configured")
|
||||
}
|
||||
|
||||
documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query document id failed, document_external_id=%s, err=%+v", documentExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed")
|
||||
}
|
||||
if documentID <= 0 {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
userID, err := s.userRepo.GetIDByExternalID(userExternalID).Exec()
|
||||
if err != nil || userID <= 0 {
|
||||
logrus.Errorf("[Service layer: DocumentService] query user id failed, user_external_id=%s, err=%+v", userExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusUserNotFound, "user not found")
|
||||
}
|
||||
|
||||
safeName := strings.TrimSpace(fileName)
|
||||
if safeName == "" {
|
||||
safeName = "attachment.bin"
|
||||
}
|
||||
contentType = strings.TrimSpace(contentType)
|
||||
if contentType == "" {
|
||||
contentType = http.DetectContentType(file)
|
||||
}
|
||||
|
||||
attachmentExternalID := utils.GenerateExternalID(utils.ExternalIDContextAttachment)
|
||||
objectName := fmt.Sprintf(
|
||||
"attachments/%s/%s-%d%s",
|
||||
documentExternalID,
|
||||
attachmentExternalID,
|
||||
time.Now().UnixNano(),
|
||||
resolveAttachmentExt(safeName, contentType),
|
||||
)
|
||||
|
||||
err = storage.PutFile(
|
||||
config.GlobalConfig.Minio.Bucket,
|
||||
objectName,
|
||||
bytes.NewReader(file),
|
||||
int64(len(file)),
|
||||
contentType,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] upload attachment to minio failed, document_external_id=%s, object_name=%s, err=%+v", documentExternalID, objectName, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "upload attachment failed")
|
||||
}
|
||||
|
||||
attachment := &model.Attachment{
|
||||
ExternalID: attachmentExternalID,
|
||||
DocumentID: documentID,
|
||||
Name: safeName,
|
||||
Path: objectName,
|
||||
Size: int64(len(file)),
|
||||
MimeType: contentType,
|
||||
UserID: userID,
|
||||
}
|
||||
if err = s.attachmentRepo.Create(attachment); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] save attachment meta failed, document_external_id=%s, attachment_external_id=%s, err=%+v", documentExternalID, attachmentExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusWriteDBError, "save attachment failed")
|
||||
}
|
||||
|
||||
return &dto.AttachmentResponse{
|
||||
ExternalID: attachment.ExternalID,
|
||||
DocumentExternalID: documentExternalID,
|
||||
Name: attachment.Name,
|
||||
Size: attachment.Size,
|
||||
MimeType: attachment.MimeType,
|
||||
Path: attachment.Path,
|
||||
URL: storage.BuildPublicObjectPath(attachment.Path),
|
||||
CreatedAt: attachment.CreatedAt,
|
||||
UpdatedAt: attachment.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) ListAttachments(ctx context.Context, documentExternalID string) ([]*dto.AttachmentResponse, error) {
|
||||
if s == nil || strings.TrimSpace(documentExternalID) == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid list attachments request")
|
||||
}
|
||||
if config.GlobalConfig == nil || strings.TrimSpace(config.GlobalConfig.Minio.Bucket) == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "attachment storage is not configured")
|
||||
}
|
||||
|
||||
documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query document id failed, document_external_id=%s, err=%+v", documentExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed")
|
||||
}
|
||||
if documentID <= 0 {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
attachments, err := s.attachmentRepo.ListByDocumentID(documentID)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] list attachments failed, document_external_id=%s, document_id=%d, err=%+v", documentExternalID, documentID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "list attachments failed")
|
||||
}
|
||||
|
||||
resp := make([]*dto.AttachmentResponse, 0, len(attachments))
|
||||
for _, attachment := range attachments {
|
||||
resp = append(resp, &dto.AttachmentResponse{
|
||||
ExternalID: attachment.ExternalID,
|
||||
DocumentExternalID: documentExternalID,
|
||||
Name: attachment.Name,
|
||||
Size: attachment.Size,
|
||||
MimeType: attachment.MimeType,
|
||||
Path: attachment.Path,
|
||||
URL: storage.BuildPublicObjectPath(attachment.Path),
|
||||
CreatedAt: attachment.CreatedAt,
|
||||
UpdatedAt: attachment.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) DeleteAttachment(ctx context.Context, documentExternalID, attachmentExternalID string) error {
|
||||
if s == nil || strings.TrimSpace(documentExternalID) == "" || strings.TrimSpace(attachmentExternalID) == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid delete attachment request")
|
||||
}
|
||||
if config.GlobalConfig == nil || strings.TrimSpace(config.GlobalConfig.Minio.Bucket) == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "attachment storage is not configured")
|
||||
}
|
||||
|
||||
documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query document id failed, document_external_id=%s, err=%+v", documentExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed")
|
||||
}
|
||||
if documentID <= 0 {
|
||||
return status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
attachment, err := s.attachmentRepo.GetByExternalIDAndDocumentID(attachmentExternalID, documentID)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query attachment failed, document_external_id=%s, attachment_external_id=%s, err=%+v", documentExternalID, attachmentExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "attachment not found")
|
||||
}
|
||||
|
||||
if err = storage.DeleteFile(config.GlobalConfig.Minio.Bucket, attachment.Path); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] delete attachment object failed, attachment_external_id=%s, path=%s, err=%+v", attachmentExternalID, attachment.Path, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "delete attachment failed")
|
||||
}
|
||||
if err = s.attachmentRepo.DeleteByID(attachment.ID); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] delete attachment meta failed, attachment_external_id=%s, err=%+v", attachmentExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "delete attachment failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveAttachmentExt(filename, contentType string) string {
|
||||
ext := strings.TrimSpace(strings.ToLower(filepath.Ext(filename)))
|
||||
if ext != "" {
|
||||
return ext
|
||||
}
|
||||
exts, err := mime.ExtensionsByType(contentType)
|
||||
if err == nil && len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
return ".bin"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/document_repo"
|
||||
internal_dto "github.com/XingfenD/yoresee_doc/internal/service/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *DocumentService) ConvertToDocumentResponse(doc *model.Document) *dto.DocumentMetaResponse {
|
||||
return dto.NewDocumentMetaResponseFromModel(doc)
|
||||
}
|
||||
|
||||
func (s *DocumentService) buildListDocumentsOperation(req *internal_dto.DocumentsListReq) (*document_repo.DocumentsListOperation, error) {
|
||||
if s == nil || s.documentRepo == nil {
|
||||
return nil, status.StatusServiceInternalError
|
||||
}
|
||||
if req == nil {
|
||||
return nil, status.StatusInternalParamsError
|
||||
}
|
||||
listOp := s.documentRepo.ListDocuments(&model.Document{})
|
||||
if req.MetaArgs != nil {
|
||||
listOp = listOp.WithUserID(req.MetaArgs.UserID).
|
||||
WithParentID(req.MetaArgs.ParentID).
|
||||
WithKnowledgeID(req.MetaArgs.KnowledgeID).
|
||||
WithListOwnDoc(req.ListOwnDoc).
|
||||
WithDirectoryOnly(req.DirectoryOnly)
|
||||
}
|
||||
if req.SearchDocIDs != nil {
|
||||
listOp = listOp.WithIDs(req.SearchDocIDs)
|
||||
}
|
||||
if req.FilterArgs != nil {
|
||||
titleKeyword := req.FilterArgs.TitleKeyword
|
||||
if req.SearchDocIDs != nil {
|
||||
titleKeyword = nil
|
||||
}
|
||||
listOp = listOp.WithTitleKeyword(titleKeyword).
|
||||
WithType(req.FilterArgs.DocType).
|
||||
WithTags(req.FilterArgs.Tags).
|
||||
WithCreateTimeRange(req.FilterArgs.CreateTimeRangeStart, req.FilterArgs.CreateTimeRangeEnd).
|
||||
WithUpdateTimeRange(req.FilterArgs.UpdateTimeRangeStart, req.FilterArgs.UpdateTimeRangeEnd)
|
||||
}
|
||||
listOp = listOp.WithSort(req.SortArgs.Field, req.SortArgs.Desc).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize)
|
||||
|
||||
return listOp, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) buildDocumentTree(rootDocs []model.Document, allDescendants []*model.Document) []*dto.DocumentMetaResponse {
|
||||
docMap := make(map[int64]*dto.DocumentMetaResponse)
|
||||
var rootResponses []*dto.DocumentMetaResponse
|
||||
|
||||
for i := range rootDocs {
|
||||
resp := s.ConvertToDocumentResponse(&rootDocs[i])
|
||||
docMap[rootDocs[i].ID] = resp
|
||||
rootResponses = append(rootResponses, resp)
|
||||
}
|
||||
|
||||
for _, doc := range allDescendants {
|
||||
childResp := s.ConvertToDocumentResponse(doc)
|
||||
docMap[doc.ID] = childResp
|
||||
|
||||
if doc.ParentID != 0 {
|
||||
parentResp, exists := docMap[doc.ParentID]
|
||||
if exists {
|
||||
parentResp.Children = append(parentResp.Children, childResp)
|
||||
parentResp.HasChildren = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootResponses
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/domain_event"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) Create(ctx context.Context, req *dto.CreateDocumentRequest) (*dto.CreateDocumentResponse, error) {
|
||||
if err := validateCreateDocumentReq(req); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] validateCreateDocumentReq failed, err=%+v", err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "invalid create document request")
|
||||
}
|
||||
|
||||
// TODO: redis support
|
||||
docExternalID := utils.GenerateExternalID(utils.ExternalIDContextDocument)
|
||||
err := utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
docModel := &model.Document{
|
||||
ExternalID: docExternalID,
|
||||
Title: req.Title,
|
||||
Type: doc_type_mapper.ToModelType(req.Type),
|
||||
ContainerType: doc_container_mapper.ToModelType(req.ContainerType),
|
||||
Summary: "",
|
||||
IsPublic: req.IsPublic,
|
||||
Content: "",
|
||||
Path: "0",
|
||||
Depth: 0,
|
||||
}
|
||||
|
||||
// query user_id
|
||||
userID, err := s.userRepo.GetIDByExternalID(*req.CreatorExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
docModel.UserID = userID
|
||||
|
||||
// query knowledge_id
|
||||
if req.ContainerType == dto.ContainerType_KnowledgeBase {
|
||||
kbID, err := s.kbRepo.GetIDByExternalID(*req.KnowledgeExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusKnowledgeBaseNotFound
|
||||
}
|
||||
docModel.KnowledgeID = &kbID
|
||||
}
|
||||
|
||||
// TODO: permission check for knowledgebase
|
||||
|
||||
// query parent_id
|
||||
if req.ParentExternalID != nil {
|
||||
parentDocID, err := s.documentRepo.GetIDByExternalID(*req.ParentExternalID).WithTx(tx).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
docModel.ParentID = parentDocID
|
||||
}
|
||||
|
||||
// apply template content if provided
|
||||
if req.TemplateID != nil && *req.TemplateID > 0 {
|
||||
tpl, err := s.templateRepo.GetByID(*req.TemplateID).WithTx(tx).Exec()
|
||||
if err != nil || tpl == nil {
|
||||
logrus.Errorf("template not found: err:%+v", err)
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template not found")
|
||||
}
|
||||
docModel.Content = tpl.Content
|
||||
}
|
||||
|
||||
// create document meta
|
||||
err = s.documentRepo.Create(docModel).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
status.GenErrWithCustomMsg(status.StatusWriteDBError, "create document meta failed")
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
if err := s.documentRepo.UpdatePathDepth(docModel.ID, docModel.ParentID).WithTx(tx).Exec(); err != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "update document path depth failed")
|
||||
}
|
||||
|
||||
// create doc version
|
||||
ver := &model.DocumentVersion{
|
||||
DocumentID: docModel.ID,
|
||||
Content: docModel.Content,
|
||||
UserID: userID,
|
||||
Title: docModel.Title,
|
||||
ChangeSummary: "Create the document",
|
||||
}
|
||||
err = s.docVersionRepo.Create(ver).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create version failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] Create err: %+v", err)
|
||||
return nil, status.StatusWriteDBError
|
||||
}
|
||||
|
||||
if createdDoc, err := s.documentRepo.GetByExternalID(docExternalID).Exec(ctx); err == nil {
|
||||
if err := s.documentRepo.BumpSubtreeVersionsByPath(ctx, createdDoc.Path); err != nil {
|
||||
logrus.Warnf("bump subtree version failed: %v", err)
|
||||
}
|
||||
}
|
||||
if req.TemplateID != nil && req.CreatorExternalID != nil && *req.CreatorExternalID != "" {
|
||||
_ = s.CreateRecentTemplate(&dto.CreateRecentTemplateRequest{
|
||||
UserExternalID: *req.CreatorExternalID,
|
||||
TemplateID: *req.TemplateID,
|
||||
AccessTime: time.Now(),
|
||||
})
|
||||
}
|
||||
if err := domain_event.PublishDocumentUpsertEvent(ctx, docExternalID); err != nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] publish search sync event failed, external_id=%s, err=%+v", docExternalID, err)
|
||||
}
|
||||
|
||||
return &dto.CreateDocumentResponse{
|
||||
ExternalID: docExternalID,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *DocumentService) CreateRecentTemplate(req *dto.CreateRecentTemplateRequest) error {
|
||||
if req == nil || req.UserExternalID == "" || req.TemplateID <= 0 {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
err = s.templateRepo.CreateRecentTemplate(&model.RecentTemplate{
|
||||
UserID: userID,
|
||||
TemplateID: req.TemplateID,
|
||||
AccessedAt: req.AccessTime,
|
||||
}).Exec()
|
||||
if err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/template_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type templatePayload struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Content string `json:"content"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (s *DocumentService) CreateTemplate(ctx context.Context, req *dto.CreateTemplateRequest) error {
|
||||
if err := validateCreateTemplateReq(req); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] validateCreateTemplateReq failed, err=%+v", err)
|
||||
return status.GenErrWithCustomMsg(err, "invalid create template request")
|
||||
}
|
||||
_ = ctx
|
||||
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
var kbID *int64
|
||||
if template_container_mapper.RequiresKnowledgeBaseID(req.TargetContainer) {
|
||||
id, err := s.kbRepo.GetIDByExternalID(*req.KnowledgeBaseExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusKnowledgeBaseNotFound
|
||||
}
|
||||
kbID = &id
|
||||
}
|
||||
|
||||
name, description, content, tags := parseTemplateContent(req.TemplateContent)
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template content is empty")
|
||||
}
|
||||
|
||||
templateType := req.Type
|
||||
if !doc_type_mapper.IsSupportedDTOType(templateType) {
|
||||
templateType = doc_type_mapper.DefaultDTOType()
|
||||
}
|
||||
|
||||
scope := template_container_mapper.ToScope(req.TargetContainer)
|
||||
isPublic := template_container_mapper.ToIsPublic(req.TargetContainer)
|
||||
templateModel := &model.Template{
|
||||
Name: name,
|
||||
Description: description,
|
||||
DocumentType: doc_type_mapper.ToModelType(templateType),
|
||||
Content: content,
|
||||
UserID: userID,
|
||||
Scope: scope,
|
||||
KnowledgeBaseID: kbID,
|
||||
IsPublic: isPublic,
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
if err := s.templateRepo.Create(templateModel).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseTemplateContent(raw string) (name, description, content string, tags []string) {
|
||||
content = raw
|
||||
var payload templatePayload
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err == nil {
|
||||
if payload.Content != "" {
|
||||
content = payload.Content
|
||||
}
|
||||
name = payload.Name
|
||||
description = payload.Description
|
||||
tags = payload.Tags
|
||||
}
|
||||
if strings.TrimSpace(name) == "" {
|
||||
name = deriveTemplateName(content)
|
||||
}
|
||||
if len(name) > 100 {
|
||||
name = truncateRunes(name, 100)
|
||||
}
|
||||
return name, description, content, tags
|
||||
}
|
||||
|
||||
func deriveTemplateName(content string) string {
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
trimmed = strings.TrimLeft(trimmed, "#*-+ ")
|
||||
trimmed = strings.TrimSpace(trimmed)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
return "Untitled Template"
|
||||
}
|
||||
|
||||
func truncateRunes(s string, max int) string {
|
||||
if max <= 0 {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(s)
|
||||
if len(runes) <= max {
|
||||
return s
|
||||
}
|
||||
return string(runes[:max])
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/attachment_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/document_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/document_version_repo"
|
||||
doc_yjs_snapshot_repo "github.com/XingfenD/yoresee_doc/internal/repository/document_yjs_snapshot_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/knowledge_base_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/template_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DocumentService struct {
|
||||
db *gorm.DB
|
||||
documentRepo *document_repo.DocumentRepository
|
||||
userRepo *user_repo.UserRepository
|
||||
kbRepo *knowledge_base_repo.KnowledgeBaseRepository
|
||||
docVersionRepo *document_version_repo.DocumentVersionRepository
|
||||
snapshotRepo *doc_yjs_snapshot_repo.DocumentYjsSnapshotRepository
|
||||
templateRepo *template_repo.TemplateRepository
|
||||
attachmentRepo *attachment_repo.AttachmentRepository
|
||||
}
|
||||
|
||||
func NewDocumentService(repos *repository.Repositories) *DocumentService {
|
||||
return &DocumentService{
|
||||
db: repos.DB,
|
||||
documentRepo: repos.Document,
|
||||
userRepo: repos.User,
|
||||
kbRepo: repos.KnowledgeBase,
|
||||
docVersionRepo: repos.DocumentVersion,
|
||||
snapshotRepo: repos.DocumentYjsSnapshot,
|
||||
templateRepo: repos.Template,
|
||||
attachmentRepo: repos.Attachment,
|
||||
}
|
||||
}
|
||||
|
||||
var DocumentSvc *DocumentService
|
||||
|
||||
func (s *DocumentService) DeleteDocument(id int64) error {
|
||||
return s.documentRepo.Delete(id).Exec()
|
||||
}
|
||||
|
||||
func (s *DocumentService) DeleteDocumentByExternalID(ctx context.Context, externalID string) error {
|
||||
docID, err := s.documentRepo.GetIDByExternalID(externalID).Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.DeleteDocument(docID)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *DocumentService) ListDocumentVersions(ctx context.Context, documentExternalID string, page, pageSize int32) ([]*dto.DocumentVersionResponse, int64, error) {
|
||||
if s == nil || strings.TrimSpace(documentExternalID) == "" {
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusParamError, "invalid list document versions request")
|
||||
}
|
||||
|
||||
p := int(page)
|
||||
if p <= 0 {
|
||||
p = 1
|
||||
}
|
||||
ps := int(pageSize)
|
||||
if ps <= 0 {
|
||||
ps = 20
|
||||
}
|
||||
if ps > 100 {
|
||||
ps = 100
|
||||
}
|
||||
offset := (p - 1) * ps
|
||||
|
||||
documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query document id for list versions failed, document_external_id=%s, err=%+v", documentExternalID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed")
|
||||
}
|
||||
if documentID <= 0 {
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
items, total, err := s.docVersionRepo.ListByDocumentID(documentID, offset, ps)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] list document versions failed, document_external_id=%s, document_id=%d, err=%+v", documentExternalID, documentID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusReadDBError, "list document versions failed")
|
||||
}
|
||||
|
||||
resp := make([]*dto.DocumentVersionResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
resp = append(resp, &dto.DocumentVersionResponse{
|
||||
Version: item.Version,
|
||||
Title: item.Title,
|
||||
ChangeSummary: item.ChangeSummary,
|
||||
CreatedAt: item.CreatedAt,
|
||||
})
|
||||
}
|
||||
return resp, total, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) GetDocumentVersionContent(ctx context.Context, documentExternalID string, version int32) (*dto.DocumentVersionResponse, error) {
|
||||
if s == nil || strings.TrimSpace(documentExternalID) == "" || version <= 0 {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid get document version request")
|
||||
}
|
||||
|
||||
documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query document id for get version failed, document_external_id=%s, err=%+v", documentExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed")
|
||||
}
|
||||
if documentID <= 0 {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
item, err := s.docVersionRepo.GetByDocumentIDAndVersion(documentID, int(version))
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] get document version failed, document_external_id=%s, document_id=%d, version=%d, err=%+v", documentExternalID, documentID, version, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document version not found")
|
||||
}
|
||||
|
||||
return &dto.DocumentVersionResponse{
|
||||
Version: item.Version,
|
||||
Title: item.Title,
|
||||
Content: item.Content,
|
||||
ChangeSummary: item.ChangeSummary,
|
||||
CreatedAt: item.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *DocumentService) GetDocumentByExternalID(ctx context.Context, externalID string) (*dto.DocumentResponse, error) {
|
||||
docModel, err := s.documentRepo.GetByExternalID(externalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] GetDocumentByExternalID failed, external_id=%s, err=%+v", externalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
return dto.NewDocumentResponseFromModel(docModel), nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *DocumentService) GetDocumentSettings(ctx context.Context, req *dto.GetDocumentSettingsRequest) (*dto.DocumentSettingsResponse, error) {
|
||||
if req == nil || req.ExternalID == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
|
||||
// Settings page expects real-time value after save/update, so read from DB directly
|
||||
// to avoid returning a stale cached model.
|
||||
docModel, err := s.documentRepo.GetByExternalID(req.ExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] GetDocumentSettings failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
// TODO: merge fields from document_setting table when that table is introduced.
|
||||
return &dto.DocumentSettingsResponse{
|
||||
IsPublic: docModel.IsPublic,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) GetTemplateByID(templateID int64) (*dto.TemplateResponse, error) {
|
||||
if templateID <= 0 {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
|
||||
tpl, err := s.templateRepo.GetByID(templateID).Exec()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "template not found")
|
||||
}
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
if tpl == nil {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "template not found")
|
||||
}
|
||||
|
||||
kbExternalID := ""
|
||||
if tpl.KnowledgeBaseID != nil {
|
||||
kbs, err := s.kbRepo.MGetKnowledgeBaseByIDs([]int64{*tpl.KnowledgeBaseID}).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
if len(kbs) > 0 {
|
||||
kbExternalID = kbs[0].ExternalID
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.TemplateResponse{
|
||||
ID: tpl.ID,
|
||||
Name: tpl.Name,
|
||||
Description: tpl.Description,
|
||||
Content: tpl.Content,
|
||||
Type: doc_type_mapper.FromModelType(tpl.DocumentType),
|
||||
Scope: tpl.Scope,
|
||||
KnowledgeBaseExternalID: kbExternalID,
|
||||
Tags: tpl.Tags,
|
||||
CreatedAt: tpl.CreatedAt,
|
||||
UpdatedAt: tpl.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *DocumentService) GetDocumentTypeByExternalID(ctx context.Context, externalID string) (model.DocumentType, error) {
|
||||
docModel, err := s.documentRepo.GetByExternalID(externalID).Exec(ctx)
|
||||
if err != nil {
|
||||
return "", status.StatusDocumentNotFound
|
||||
}
|
||||
return docModel.Type, nil
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
internal_dto "github.com/XingfenD/yoresee_doc/internal/service/dto"
|
||||
)
|
||||
|
||||
func (s *DocumentService) listDocuments(ctx context.Context, req *internal_dto.DocumentsListReq) ([]*dto.DocumentMetaResponse, int64, error) {
|
||||
listOp, err := s.buildListDocumentsOperation(req)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] buildListDocumentsOperation failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build document list operation failed")
|
||||
}
|
||||
models, total, err := listOp.ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] list documents failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "list documents failed")
|
||||
}
|
||||
|
||||
// if children is not needed or depth less than 1, return directly
|
||||
if req.Options == nil || !req.Options.IncludeChildren || (req.Options.Depth != nil && *req.Options.Depth < 1) {
|
||||
return s.buildDocumentTree(models, nil), total, nil
|
||||
}
|
||||
|
||||
parentIDs := make([]int64, len(models))
|
||||
for i := range models {
|
||||
parentIDs[i] = models[i].ID
|
||||
}
|
||||
|
||||
getOp := s.getDocumentsWithDescendants(parentIDs).
|
||||
WithDirectoryOnly(req.DirectoryOnly)
|
||||
if req.Options != nil {
|
||||
getOp = getOp.WithDepth(req.Options.Depth)
|
||||
}
|
||||
|
||||
allDescendants, err := getOp.Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] getDocumentsWithDescendants failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "list document descendants failed")
|
||||
}
|
||||
|
||||
docs := s.buildDocumentTree(models, allDescendants)
|
||||
return docs, total, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) ListDocumentsByExternal(ctx context.Context, req *dto.ListDocumentsByExternalRequest) ([]*dto.DocumentMetaResponse, int64, error) {
|
||||
var userID *int64
|
||||
if req == nil {
|
||||
return nil, 0, status.StatusInternalParamsError
|
||||
}
|
||||
if req.ExternalArgs != nil && req.ExternalArgs.UserExternalID != nil && *req.ExternalArgs.UserExternalID != "" {
|
||||
id, err := s.userRepo.GetIDByExternalID(*req.ExternalArgs.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query user id by external id failed, external_id=%s, err=%+v", *req.ExternalArgs.UserExternalID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusUserNotFound, "user not found")
|
||||
}
|
||||
userID = &id
|
||||
}
|
||||
|
||||
var parentID int64
|
||||
if req.ExternalArgs != nil && req.ExternalArgs.RootDocumentExternalID != nil && *req.ExternalArgs.RootDocumentExternalID != "" {
|
||||
doc, err := s.documentRepo.GetByExternalID(*req.ExternalArgs.RootDocumentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query root document failed, external_id=%s, err=%+v", *req.ExternalArgs.RootDocumentExternalID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "root document not found")
|
||||
}
|
||||
parentID = doc.ID
|
||||
}
|
||||
|
||||
var knowledgeID *int64
|
||||
if req.ExternalArgs != nil && req.ExternalArgs.KnowledgeExternalID != nil && *req.ExternalArgs.KnowledgeExternalID != "" {
|
||||
id, err := s.kbRepo.GetIDByExternalID(*req.ExternalArgs.KnowledgeExternalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query knowledge base id failed, external_id=%s, err=%+v", *req.ExternalArgs.KnowledgeExternalID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusKnowledgeBaseNotFound, "knowledge base not found")
|
||||
}
|
||||
knowledgeID = &id
|
||||
}
|
||||
|
||||
internalReq := &internal_dto.DocumentsListReq{
|
||||
MetaArgs: &internal_dto.DocumentsListMetaArgs{
|
||||
UserID: userID,
|
||||
ParentID: &parentID, // default to root
|
||||
KnowledgeID: knowledgeID,
|
||||
},
|
||||
ListDocumentsBaseArgs: dto.ListDocumentsBaseArgs{
|
||||
ListOwnDoc: req.ListOwnDoc,
|
||||
DirectoryOnly: req.DirectoryOnly,
|
||||
},
|
||||
FilterArgs: req.FilterArgs,
|
||||
SortArgs: req.SortArgs,
|
||||
Pagination: req.Pagination,
|
||||
Options: req.Options,
|
||||
}
|
||||
s.applyElasticsearchKeywordFilter(ctx, internalReq)
|
||||
|
||||
return s.listDocuments(ctx, internalReq)
|
||||
}
|
||||
|
||||
type GetDocumentsWithDescendantsOption struct {
|
||||
DirectoryOnly bool
|
||||
}
|
||||
|
||||
type GetDocumentsWithDescendantsOperation struct {
|
||||
svc *DocumentService
|
||||
parentIDs []int64
|
||||
depth *int
|
||||
|
||||
directoryOnly bool
|
||||
}
|
||||
|
||||
func (s *DocumentService) getDocumentsWithDescendants(parentIDs []int64) *GetDocumentsWithDescendantsOperation {
|
||||
return &GetDocumentsWithDescendantsOperation{
|
||||
svc: s,
|
||||
parentIDs: parentIDs,
|
||||
}
|
||||
}
|
||||
|
||||
func (op *GetDocumentsWithDescendantsOperation) WithDepth(depth *int) *GetDocumentsWithDescendantsOperation {
|
||||
op.depth = depth
|
||||
return op
|
||||
}
|
||||
|
||||
func (op *GetDocumentsWithDescendantsOperation) WithDirectoryOnly(with bool) *GetDocumentsWithDescendantsOperation {
|
||||
op.directoryOnly = with
|
||||
return op
|
||||
}
|
||||
|
||||
func (op *GetDocumentsWithDescendantsOperation) Exec(ctx context.Context) ([]*model.Document, error) {
|
||||
if len(op.parentIDs) == 0 {
|
||||
return []*model.Document{}, nil
|
||||
}
|
||||
|
||||
allDocs := make([]*model.Document, 0)
|
||||
seen := make(map[int64]bool)
|
||||
|
||||
for _, rootParentID := range op.parentIDs {
|
||||
docs, err := op.svc.documentRepo.GetSubtree(rootParentID).
|
||||
WithDepth(op.depth).
|
||||
WithDirectoryOnly(op.directoryOnly).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] get subtree failed, root_parent_id=%d, err=%+v", rootParentID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "get document subtree failed")
|
||||
}
|
||||
|
||||
for _, doc := range docs {
|
||||
if !seen[doc.ID] {
|
||||
seen[doc.ID] = true
|
||||
allDocs = append(allDocs, doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allDocs, nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *DocumentService) ListRecentTemplates(req *dto.ListRecentTemplatesRequest) ([]*dto.TemplateResponse, int64, error) {
|
||||
if req == nil || req.UserExternalID == "" {
|
||||
return nil, 0, status.StatusInternalParamsError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusUserNotFound
|
||||
}
|
||||
|
||||
// TODO: filter the recent templates by kb
|
||||
recentRecords, total, err := s.templateRepo.ListRecentTemplates(userID).
|
||||
WithTimeRange(req.StartTime, req.EndTime).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
if len(recentRecords) == 0 {
|
||||
return []*dto.TemplateResponse{}, total, nil
|
||||
}
|
||||
|
||||
tplIDs := make([]int64, 0, len(recentRecords))
|
||||
for _, r := range recentRecords {
|
||||
tplIDs = append(tplIDs, r.TemplateID)
|
||||
}
|
||||
|
||||
tplModels, err := s.templateRepo.MGetByIDs(tplIDs).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
tplMap := make(map[int64]*model.Template, len(tplModels))
|
||||
for _, tpl := range tplModels {
|
||||
tplMap[tpl.ID] = tpl
|
||||
}
|
||||
|
||||
// map knowledge_base external ids
|
||||
kbExternalIDMap := make(map[int64]string)
|
||||
kbIDs := make([]int64, 0)
|
||||
for _, tpl := range tplModels {
|
||||
if tpl.KnowledgeBaseID == nil || *tpl.KnowledgeBaseID == 0 {
|
||||
continue
|
||||
}
|
||||
kbIDs = append(kbIDs, *tpl.KnowledgeBaseID)
|
||||
}
|
||||
if len(kbIDs) > 0 {
|
||||
kbs, err := s.kbRepo.MGetKnowledgeBaseByIDs(kbIDs).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
for _, kb := range kbs {
|
||||
kbExternalIDMap[kb.ID] = kb.ExternalID
|
||||
}
|
||||
}
|
||||
|
||||
resp := make([]*dto.TemplateResponse, 0, len(recentRecords))
|
||||
for _, record := range recentRecords {
|
||||
tpl := tplMap[record.TemplateID]
|
||||
if tpl == nil {
|
||||
continue
|
||||
}
|
||||
kbExternalID := ""
|
||||
if tpl.KnowledgeBaseID != nil {
|
||||
kbExternalID = kbExternalIDMap[*tpl.KnowledgeBaseID]
|
||||
}
|
||||
resp = append(resp, &dto.TemplateResponse{
|
||||
ID: tpl.ID,
|
||||
Name: tpl.Name,
|
||||
Description: tpl.Description,
|
||||
Content: tpl.Content,
|
||||
Type: doc_type_mapper.FromModelType(tpl.DocumentType),
|
||||
Scope: tpl.Scope,
|
||||
KnowledgeBaseExternalID: kbExternalID,
|
||||
Tags: tpl.Tags,
|
||||
CreatedAt: tpl.CreatedAt,
|
||||
UpdatedAt: tpl.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return resp, total, nil
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/template_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
internal_dto "github.com/XingfenD/yoresee_doc/internal/service/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/bytedance/gg/gslice"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type templateListOperation struct {
|
||||
req *internal_dto.TemplateListReq
|
||||
srvc *DocumentService
|
||||
}
|
||||
|
||||
func (s *DocumentService) listTemplates(req *internal_dto.TemplateListReq) *templateListOperation {
|
||||
return &templateListOperation{
|
||||
req: req,
|
||||
srvc: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (op *templateListOperation) ExecWithTotal() ([]*dto.TemplateResponse, int64, error) {
|
||||
listOp, err := op.srvc.buildListTemplateOperation(op.req)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] buildListTemplateOperation failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build template list operation failed")
|
||||
}
|
||||
templates, total, err := listOp.ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] list templates failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "list templates failed")
|
||||
}
|
||||
|
||||
kbExternalIDMap := make(map[int64]string)
|
||||
kbIDs := make([]int64, 0)
|
||||
for _, tmpl := range templates {
|
||||
if tmpl.KnowledgeBaseID == nil || *tmpl.KnowledgeBaseID == 0 {
|
||||
continue
|
||||
}
|
||||
kbIDs = append(kbIDs, *tmpl.KnowledgeBaseID)
|
||||
}
|
||||
kbIDs = gslice.Uniq(kbIDs)
|
||||
if len(kbIDs) > 0 {
|
||||
kbs, err := op.srvc.kbRepo.MGetKnowledgeBaseByIDs(kbIDs).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] list templates get knowledge bases failed: %+v", err)
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
for _, kb := range kbs {
|
||||
kbExternalIDMap[kb.ID] = kb.ExternalID
|
||||
}
|
||||
}
|
||||
|
||||
resp := make([]*dto.TemplateResponse, 0, len(templates))
|
||||
for _, tmpl := range templates {
|
||||
kbExternalID := ""
|
||||
if tmpl.KnowledgeBaseID != nil {
|
||||
kbExternalID = kbExternalIDMap[*tmpl.KnowledgeBaseID]
|
||||
}
|
||||
resp = append(resp, &dto.TemplateResponse{
|
||||
ID: tmpl.ID,
|
||||
Name: tmpl.Name,
|
||||
Description: tmpl.Description,
|
||||
Content: tmpl.Content,
|
||||
Type: doc_type_mapper.FromModelType(tmpl.DocumentType),
|
||||
Scope: tmpl.Scope,
|
||||
KnowledgeBaseExternalID: kbExternalID,
|
||||
Tags: tmpl.Tags,
|
||||
CreatedAt: tmpl.CreatedAt,
|
||||
UpdatedAt: tmpl.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return resp, total, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) ListTemplatesByExternal(req *dto.TemplateListByExternalRequest) ([]*dto.TemplateResponse, int64, error) {
|
||||
var creatorID *int64
|
||||
if req.CreatorExternalID != "" {
|
||||
id, err := s.userRepo.GetIDByExternalID(req.CreatorExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusUserNotFound
|
||||
}
|
||||
creatorID = &id
|
||||
}
|
||||
|
||||
internalReq, err := buildTemplateListReqFromExternal(req, creatorID)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] buildTemplateListReqFromExternal failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build template list request failed")
|
||||
}
|
||||
list, total, err := s.listTemplates(internalReq).ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] ListTemplatesByExternal failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "list templates failed")
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
func buildTemplateListReqFromExternal(req *dto.TemplateListByExternalRequest, creatorID *int64) (*internal_dto.TemplateListReq, error) {
|
||||
filter := &dto.TemplateListFilterArgs{}
|
||||
if req.FilterArgs != nil {
|
||||
*filter = *req.FilterArgs
|
||||
}
|
||||
|
||||
return &internal_dto.TemplateListReq{
|
||||
CreatorID: creatorID,
|
||||
FilterArgs: filter,
|
||||
SortArgs: req.SortArgs,
|
||||
Pagination: req.Pagination,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) buildListTemplateOperation(req *internal_dto.TemplateListReq) (*templateListOperationBuilder, error) {
|
||||
builder := &templateListOperationBuilder{
|
||||
req: req,
|
||||
srvc: s,
|
||||
}
|
||||
return builder, nil
|
||||
}
|
||||
|
||||
type templateListOperationBuilder struct {
|
||||
req *internal_dto.TemplateListReq
|
||||
srvc *DocumentService
|
||||
}
|
||||
|
||||
func (b *templateListOperationBuilder) ExecWithTotal() ([]*model.Template, int64, error) {
|
||||
op := b.srvc.templateRepo.List(&model.Template{})
|
||||
|
||||
if b.req.CreatorID != nil {
|
||||
op = op.WithUserID(b.req.CreatorID)
|
||||
}
|
||||
|
||||
if b.req.FilterArgs != nil {
|
||||
if b.req.FilterArgs.TargetContainer != nil {
|
||||
if template_container_mapper.RequiresKnowledgeBaseID(*b.req.FilterArgs.TargetContainer) &&
|
||||
(b.req.FilterArgs.KnowledgeBaseID == nil || *b.req.FilterArgs.KnowledgeBaseID == "") {
|
||||
return nil, 0, status.StatusParamError
|
||||
}
|
||||
scope := template_container_mapper.ToScope(*b.req.FilterArgs.TargetContainer)
|
||||
op = op.WithScope(&scope)
|
||||
}
|
||||
if b.req.FilterArgs.KnowledgeBaseID != nil && *b.req.FilterArgs.KnowledgeBaseID != "" {
|
||||
kbID, err := b.srvc.kbRepo.GetIDByExternalID(*b.req.FilterArgs.KnowledgeBaseID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] Get knowledge base id for template list failed, external_id=%s, err=%+v", *b.req.FilterArgs.KnowledgeBaseID, err)
|
||||
return nil, 0, status.StatusKnowledgeBaseNotFound
|
||||
}
|
||||
op = op.WithKnowledgeBaseID(&kbID)
|
||||
}
|
||||
if b.req.FilterArgs.NameKeyword != nil {
|
||||
op = op.WithNameKeyword(b.req.FilterArgs.NameKeyword)
|
||||
}
|
||||
if b.req.FilterArgs.Type != nil {
|
||||
modelType := model.DocumentType(doc_type_mapper.ToModelType(*b.req.FilterArgs.Type))
|
||||
op = op.WithDocumentType(&modelType)
|
||||
}
|
||||
}
|
||||
|
||||
sortField := b.req.SortArgs.Field
|
||||
sortDesc := b.req.SortArgs.Desc
|
||||
if sortField == "" {
|
||||
sortField = "created_at"
|
||||
sortDesc = true
|
||||
}
|
||||
op = op.WithSort(sortField, sortDesc)
|
||||
|
||||
if b.req.Pagination.Page > 0 && b.req.Pagination.PageSize > 0 {
|
||||
op = op.WithPagination(b.req.Pagination.Page, b.req.Pagination.PageSize)
|
||||
}
|
||||
|
||||
templates, total, err := op.ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] template repository list failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusReadDBError, "list templates failed")
|
||||
}
|
||||
return templates, total, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *DocumentService) RecordRecentDocument(req *dto.RecordRecentDocumentRequest) error {
|
||||
if req == nil || req.UserExternalID == "" || req.DocumentExternalID == "" {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
docID, err := s.documentRepo.GetIDByExternalID(req.DocumentExternalID).Exec(context.Background())
|
||||
if err != nil || docID == 0 {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
if err := s.documentRepo.UpsertRecentDocument(&model.RecentDocument{
|
||||
UserID: userID,
|
||||
DocumentID: docID,
|
||||
AccessedAt: time.Now(),
|
||||
}).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) ListRecentDocuments(req *dto.ListRecentDocumentsRequest) ([]*dto.DocumentResponse, int64, error) {
|
||||
if req == nil || req.UserExternalID == "" {
|
||||
return nil, 0, status.StatusInternalParamsError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusUserNotFound
|
||||
}
|
||||
|
||||
recentRecords, total, err := s.documentRepo.ListRecentDocuments(userID).
|
||||
WithTimeRange(req.StartTime, req.EndTime).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
if len(recentRecords) == 0 {
|
||||
return []*dto.DocumentResponse{}, total, nil
|
||||
}
|
||||
|
||||
docIDs := make([]int64, 0, len(recentRecords))
|
||||
for _, record := range recentRecords {
|
||||
docIDs = append(docIDs, record.DocumentID)
|
||||
}
|
||||
|
||||
docModels, err := s.documentRepo.MGetByIDs(docIDs)
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
resp := make([]*dto.DocumentResponse, 0, len(docModels))
|
||||
for _, doc := range docModels {
|
||||
resp = append(resp, dto.NewDocumentResponseFromModel(doc))
|
||||
}
|
||||
|
||||
return resp, total, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/config"
|
||||
"github.com/XingfenD/yoresee_doc/internal/search"
|
||||
internal_dto "github.com/XingfenD/yoresee_doc/internal/service/dto"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/storage"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *DocumentService) applyElasticsearchKeywordFilter(ctx context.Context, req *internal_dto.DocumentsListReq) {
|
||||
if req == nil || req.FilterArgs == nil || req.FilterArgs.TitleKeyword == nil {
|
||||
return
|
||||
}
|
||||
keyword := strings.TrimSpace(*req.FilterArgs.TitleKeyword)
|
||||
if keyword == "" {
|
||||
return
|
||||
}
|
||||
if config.GlobalConfig == nil || !config.GlobalConfig.Elasticsearch.Enabled {
|
||||
logrus.Warnf("[Service layer: DocumentService] elasticsearch is disabled, degrade to db keyword search, keyword=%s", keyword)
|
||||
return
|
||||
}
|
||||
if storage.ES == nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] elasticsearch client is nil, degrade to db keyword search, keyword=%s", keyword)
|
||||
return
|
||||
}
|
||||
|
||||
searchReq := search.DocumentSearchRequest{
|
||||
Keyword: keyword,
|
||||
DocType: req.FilterArgs.DocType,
|
||||
Tags: req.FilterArgs.Tags,
|
||||
CreateTimeRangeStart: req.FilterArgs.CreateTimeRangeStart,
|
||||
CreateTimeRangeEnd: req.FilterArgs.CreateTimeRangeEnd,
|
||||
UpdateTimeRangeStart: req.FilterArgs.UpdateTimeRangeStart,
|
||||
UpdateTimeRangeEnd: req.FilterArgs.UpdateTimeRangeEnd,
|
||||
Size: 5000,
|
||||
ListOwnDoc: req.ListOwnDoc,
|
||||
}
|
||||
if req.MetaArgs != nil {
|
||||
searchReq.UserID = req.MetaArgs.UserID
|
||||
searchReq.KnowledgeID = req.MetaArgs.KnowledgeID
|
||||
}
|
||||
|
||||
ids, err := search.SearchDocumentIDs(ctx, searchReq)
|
||||
if err != nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] elasticsearch keyword search failed, keyword=%s, err=%+v", keyword, err)
|
||||
return
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
logrus.Warnf("[Service layer: DocumentService] elasticsearch keyword search no hit, degrade to db keyword search, keyword=%s", keyword)
|
||||
return
|
||||
}
|
||||
req.SearchDocIDs = ids
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/cache"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/key"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) SaveDocumentYjsSnapshot(ctx context.Context, docExternalID string, state []byte) error {
|
||||
if len(state) == 0 {
|
||||
return status.StatusParamError
|
||||
}
|
||||
docID, err := s.documentRepo.GetIDByExternalID(docExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
if err := s.snapshotRepo.Save(docID, state).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] SaveDocumentYjsSnapshot failed, doc_external_id=%s, doc_id=%d, err=%+v", docExternalID, docID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "save document snapshot failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) SaveDocumentSnapshotAndContent(ctx context.Context, docExternalID string, state []byte, content string) (bool, error) {
|
||||
if len(state) == 0 {
|
||||
return false, status.StatusParamError
|
||||
}
|
||||
|
||||
contentChanged := false
|
||||
cacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, docExternalID)
|
||||
err := cache.DoubleDelete(
|
||||
context.Background(),
|
||||
func() error {
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
docModel, err := s.documentRepo.GetByExternalID(docExternalID).WithTx(tx).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
if err := s.snapshotRepo.Save(docModel.ID, state).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] SaveDocumentSnapshotAndContent save snapshot failed, doc_external_id=%s, doc_id=%d, err=%+v", docExternalID, docModel.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "save document snapshot failed")
|
||||
}
|
||||
|
||||
if docModel.Content == content {
|
||||
return nil
|
||||
}
|
||||
|
||||
versionModel := &model.DocumentVersion{
|
||||
DocumentID: docModel.ID,
|
||||
Title: docModel.Title,
|
||||
Content: docModel.Content,
|
||||
UserID: docModel.UserID,
|
||||
ChangeSummary: "Sync collab snapshot",
|
||||
}
|
||||
if err := s.docVersionRepo.Create(versionModel).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] SaveDocumentSnapshotAndContent create version failed, doc_external_id=%s, doc_id=%d, err=%+v", docExternalID, docModel.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create document version failed")
|
||||
}
|
||||
|
||||
nextDoc := &model.Document{
|
||||
ID: docModel.ID,
|
||||
Content: content,
|
||||
}
|
||||
if err := s.documentRepo.Update(nextDoc).WithTx(tx).UpdateContent().Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] SaveDocumentSnapshotAndContent update content failed, doc_external_id=%s, doc_id=%d, err=%+v", docExternalID, docModel.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "update document content failed")
|
||||
}
|
||||
contentChanged = true
|
||||
return nil
|
||||
})
|
||||
},
|
||||
cacheKey,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return contentChanged, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) GetDocumentYjsSnapshot(ctx context.Context, docExternalID string) ([]byte, error) {
|
||||
docID, err := s.documentRepo.GetIDByExternalID(docExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
snapshot, err := s.snapshotRepo.GetByDocID(docID).Exec()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
logrus.Errorf("[Service layer: DocumentService] GetDocumentYjsSnapshot failed, doc_external_id=%s, doc_id=%d, err=%+v", docExternalID, docID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "get document snapshot failed")
|
||||
}
|
||||
return snapshot.YjsState, nil
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/domain_event"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/cache"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/key"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) Update(ctx context.Context, req *dto.UpdateDocumentRequest) (bool, error) {
|
||||
if err := validateUpdateDocumentReq(req); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] validateUpdateDocumentReq failed, err=%+v", err)
|
||||
return false, status.GenErrWithCustomMsg(err, "invalid update document request")
|
||||
}
|
||||
|
||||
var (
|
||||
moved bool
|
||||
oldPath string
|
||||
newPath string
|
||||
)
|
||||
|
||||
cacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, req.ExternalID)
|
||||
err := cache.DoubleDelete(
|
||||
context.Background(),
|
||||
func() error {
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
oldDoc, err := s.documentRepo.GetByExternalID(req.ExternalID).WithTx(tx).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
oldPath = oldDoc.Path
|
||||
|
||||
docModel := &model.Document{
|
||||
ID: oldDoc.ID,
|
||||
}
|
||||
op := s.documentRepo.Update(docModel).WithTx(tx)
|
||||
if req.Content != nil {
|
||||
docModel.Content = *req.Content
|
||||
op.UpdateContent()
|
||||
}
|
||||
if req.Title != nil {
|
||||
docModel.Title = *req.Title
|
||||
}
|
||||
|
||||
var newParentID int64
|
||||
if req.ParentExternalID != nil {
|
||||
parentID, err := s.documentRepo.GetIDByExternalID(*req.ParentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
docModel.ParentID = parentID
|
||||
newParentID = parentID
|
||||
moved = true
|
||||
op = op.UpdateParentID()
|
||||
}
|
||||
|
||||
// create version
|
||||
versionModel := &model.DocumentVersion{
|
||||
DocumentID: oldDoc.ID,
|
||||
Title: oldDoc.Title,
|
||||
Content: oldDoc.Content,
|
||||
UserID: oldDoc.UserID,
|
||||
ChangeSummary: "",
|
||||
}
|
||||
|
||||
if err := s.docVersionRepo.Create(versionModel).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] create document version failed, document_id=%d, err=%+v", oldDoc.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create document version failed")
|
||||
}
|
||||
|
||||
// update container relation
|
||||
if req.MoveToContainer != nil {
|
||||
if !doc_container_mapper.IsSupportedDTOType(*req.MoveToContainer) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid move_to_container")
|
||||
}
|
||||
if doc_container_mapper.RequiresKnowledgeBaseID(*req.MoveToContainer) {
|
||||
kbID, err := s.kbRepo.GetIDByExternalID(*req.KnowledgeBaseExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusKnowledgeBaseNotFound
|
||||
}
|
||||
docModel.KnowledgeID = &kbID
|
||||
} else {
|
||||
docModel.KnowledgeID = nil
|
||||
}
|
||||
docModel.ContainerType = doc_container_mapper.ToModelType(*req.MoveToContainer)
|
||||
op = op.UpdateKnowledgeID().UpdateContainerType()
|
||||
}
|
||||
|
||||
if err := op.Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] update document failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "update document failed")
|
||||
}
|
||||
if moved {
|
||||
if err := s.documentRepo.MoveSubtree(oldDoc.ID, newParentID).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] move subtree failed, document_id=%d, new_parent_id=%d, err=%+v", oldDoc.ID, newParentID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "move document subtree failed")
|
||||
}
|
||||
path, err := s.documentRepo.GetPathByIDWithTx(tx, oldDoc.ID)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query new path failed, document_id=%d, err=%+v", oldDoc.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusReadDBError, "query document path failed")
|
||||
}
|
||||
newPath = path
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
},
|
||||
cacheKey,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] Update failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return false, status.GenErrWithCustomMsg(err, "update document failed")
|
||||
}
|
||||
|
||||
if moved {
|
||||
if err := s.documentRepo.BumpSubtreeVersionsByPath(ctx, oldPath); err != nil {
|
||||
logrus.Warnf("bump subtree version failed: %v", err)
|
||||
return true, nil
|
||||
}
|
||||
if newPath != "" && newPath != oldPath {
|
||||
if err := s.documentRepo.BumpSubtreeVersionsByPath(ctx, newPath); err != nil {
|
||||
logrus.Warnf("bump subtree version failed: %v", err)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := domain_event.PublishDocumentUpsertEvent(ctx, req.ExternalID); err != nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] publish search sync event failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/domain_event"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/cache"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/key"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) UpdateDocumentMeta(ctx context.Context, req *dto.UpdateDocumentMetaRequest) (bool, error) {
|
||||
if err := validateUpdateDocumentMetaReq(req); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] validateUpdateDocumentMetaReq failed, err=%+v", err)
|
||||
return false, status.GenErrWithCustomMsg(err, "invalid update document meta request")
|
||||
}
|
||||
|
||||
cacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, req.ExternalID)
|
||||
err := cache.DoubleDelete(
|
||||
context.Background(),
|
||||
func() error {
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
oldDoc, err := s.documentRepo.GetByExternalID(req.ExternalID).WithTx(tx).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
docModel := &model.Document{ID: oldDoc.ID}
|
||||
op := s.documentRepo.Update(docModel).WithTx(tx)
|
||||
|
||||
if req.Title != nil {
|
||||
docModel.Title = *req.Title
|
||||
op.UpdateTitle()
|
||||
}
|
||||
if req.Summary != nil {
|
||||
docModel.Summary = *req.Summary
|
||||
op.UpdateSummary()
|
||||
}
|
||||
if req.Tags != nil {
|
||||
docModel.Tags = *req.Tags
|
||||
op.UpdateTags()
|
||||
}
|
||||
|
||||
if err := op.Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] update document meta failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "update document meta failed")
|
||||
}
|
||||
|
||||
versionModel := &model.DocumentVersion{
|
||||
DocumentID: oldDoc.ID,
|
||||
Title: oldDoc.Title,
|
||||
Content: oldDoc.Content,
|
||||
UserID: oldDoc.UserID,
|
||||
ChangeSummary: "Update document meta",
|
||||
}
|
||||
if err := s.docVersionRepo.Create(versionModel).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] create document version failed, document_id=%d, err=%+v", oldDoc.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create document version failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
},
|
||||
cacheKey,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] UpdateDocumentMeta failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return false, status.GenErrWithCustomMsg(err, "update document meta failed")
|
||||
}
|
||||
|
||||
if err := domain_event.PublishDocumentUpsertEvent(ctx, req.ExternalID); err != nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] publish search sync event failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/domain_event"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/cache"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/key"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) UpdateDocumentSettings(ctx context.Context, req *dto.UpdateDocumentSettingsRequest) (*dto.DocumentSettingsResponse, error) {
|
||||
if req == nil || req.ExternalID == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
|
||||
cacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, req.ExternalID)
|
||||
err := cache.DoubleDelete(
|
||||
context.Background(),
|
||||
func() error {
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
oldDoc, err := s.documentRepo.GetByExternalID(req.ExternalID).WithTx(tx).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
docModel := &model.Document{
|
||||
ID: oldDoc.ID,
|
||||
IsPublic: req.IsPublic,
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Update(docModel).WithTx(tx).UpdateIsPublic().Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] update document settings failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "update document settings failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
},
|
||||
cacheKey,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] UpdateDocumentSettings failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "update document settings failed")
|
||||
}
|
||||
|
||||
if err := domain_event.PublishDocumentUpsertEvent(ctx, req.ExternalID); err != nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] publish search sync event failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
}
|
||||
|
||||
return &dto.DocumentSettingsResponse{
|
||||
IsPublic: req.IsPublic,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) UpdateTemplateSettings(ctx context.Context, req *dto.UpdateTemplateSettingsRequest) error {
|
||||
if err := validateUpdateTemplateSettingsReq(req); err != nil {
|
||||
return status.GenErrWithCustomMsg(err, "invalid update template settings request")
|
||||
}
|
||||
_ = ctx
|
||||
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
tpl, err := s.templateRepo.GetByID(req.TemplateID).Exec()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template not found")
|
||||
}
|
||||
return status.StatusReadDBError
|
||||
}
|
||||
if tpl == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template not found")
|
||||
}
|
||||
if tpl.UserID != userID {
|
||||
return status.StatusPermissionDenied
|
||||
}
|
||||
|
||||
updateOp := s.templateRepo.Update(tpl)
|
||||
|
||||
if req.Name != nil {
|
||||
nextName := strings.TrimSpace(*req.Name)
|
||||
if nextName == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template name is empty")
|
||||
}
|
||||
if len([]rune(nextName)) > 100 {
|
||||
nextName = truncateRunes(nextName, 100)
|
||||
}
|
||||
tpl.Name = nextName
|
||||
updateOp.UpdateName()
|
||||
}
|
||||
|
||||
if req.Description != nil {
|
||||
tpl.Description = strings.TrimSpace(*req.Description)
|
||||
updateOp.UpdateDescription()
|
||||
}
|
||||
|
||||
if req.IsPublic != nil {
|
||||
tpl.IsPublic = *req.IsPublic
|
||||
if *req.IsPublic {
|
||||
tpl.Scope = "system"
|
||||
} else if tpl.KnowledgeBaseID != nil {
|
||||
tpl.Scope = "knowledge_base"
|
||||
} else {
|
||||
tpl.Scope = "private"
|
||||
}
|
||||
updateOp.UpdateScope().UpdateIsPublic()
|
||||
}
|
||||
|
||||
if err := updateOp.Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
doc_container_mapper "github.com/XingfenD/yoresee_doc/internal/mapper/doc_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/template_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func validateCreateDocumentReq(req *dto.CreateDocumentRequest) error {
|
||||
if req == nil {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
if !doc_container_mapper.IsSupportedDTOType(req.ContainerType) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid document container type")
|
||||
}
|
||||
if req.ContainerType == dto.ContainerType_Own && req.KnowledgeExternalID != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusInternalParamsError, "KnowledgeExternalID not nil when container_type is own")
|
||||
}
|
||||
if req.ContainerType == dto.ContainerType_KnowledgeBase && req.KnowledgeExternalID == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusInternalParamsError, "KnowledgeExternalID is nil when container_type is knowledge_base")
|
||||
}
|
||||
|
||||
if !doc_type_mapper.IsSupportedDTOType(req.Type) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid document type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUpdateDocumentReq(req *dto.UpdateDocumentRequest) error {
|
||||
if req == nil {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
if req.ExternalID == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusInternalParamsError, "external_id is zero value")
|
||||
}
|
||||
|
||||
if req.Content == nil && req.KnowledgeBaseExternalID == nil && req.ParentExternalID == nil && req.Title == nil && req.MoveToContainer == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "no update field")
|
||||
}
|
||||
|
||||
if req.MoveToContainer != nil {
|
||||
if !doc_container_mapper.IsSupportedDTOType(*req.MoveToContainer) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid move_to_container")
|
||||
}
|
||||
if *req.MoveToContainer == dto.ContainerType_Own && req.KnowledgeBaseExternalID != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "KnowledgeBaseExternalID not nil when moving to own")
|
||||
}
|
||||
if *req.MoveToContainer == dto.ContainerType_KnowledgeBase && req.KnowledgeBaseExternalID == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "KnowledgeBaseExternalID is nil when moving to knowledge_base")
|
||||
}
|
||||
}
|
||||
if req.MoveToContainer == nil && req.KnowledgeBaseExternalID != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "MoveToContainer is nil when KnowledgeBaseExternalID provided")
|
||||
}
|
||||
|
||||
if req.MoveToContainer != nil && *req.MoveToContainer == dto.ContainerType_Own && req.ParentExternalID != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "ParentExternalID not nil when moving to own")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUpdateDocumentMetaReq(req *dto.UpdateDocumentMetaRequest) error {
|
||||
if req == nil {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
if req.ExternalID == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusInternalParamsError, "external_id is zero value")
|
||||
}
|
||||
if req.Title == nil && req.Summary == nil && req.Tags == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "no update field")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCreateTemplateReq(req *dto.CreateTemplateRequest) error {
|
||||
if req == nil {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
if req.UserExternalID == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "user external id is empty")
|
||||
}
|
||||
if req.TemplateContent == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template content is empty")
|
||||
}
|
||||
if req.Type != "" && !doc_type_mapper.IsSupportedDTOType(req.Type) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid template type")
|
||||
}
|
||||
if !template_container_mapper.IsSupportedDTOType(req.TargetContainer) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid template container")
|
||||
}
|
||||
if template_container_mapper.RequiresKnowledgeBaseID(req.TargetContainer) {
|
||||
if req.KnowledgeBaseExternalID == nil || *req.KnowledgeBaseExternalID == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "knowledge_base_id is empty")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUpdateTemplateSettingsReq(req *dto.UpdateTemplateSettingsRequest) error {
|
||||
if req == nil {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
if req.UserExternalID == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "user external id is empty")
|
||||
}
|
||||
if req.TemplateID <= 0 {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template id is invalid")
|
||||
}
|
||||
if req.Name == nil && req.Description == nil && req.IsPublic == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "no update field")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package dto
|
||||
|
||||
import "github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
|
||||
type DocumentsListMetaArgs struct {
|
||||
UserID *int64 `json:"user_id"`
|
||||
ParentID *int64 `json:"parent_id"`
|
||||
KnowledgeID *int64 `json:"knowledge_id"`
|
||||
}
|
||||
|
||||
type DocumentsListReq struct {
|
||||
MetaArgs *DocumentsListMetaArgs `json:"meta_args"`
|
||||
dto.ListDocumentsBaseArgs
|
||||
FilterArgs *dto.DocumentsListFilterArgs `json:"filter_args"`
|
||||
SearchDocIDs []int64 `json:"search_doc_ids"`
|
||||
SortArgs dto.SortArgs `json:"sort_args"`
|
||||
Pagination dto.Pagination `json:"pagination"`
|
||||
Options *dto.RecursiveOptions `json:"options"`
|
||||
}
|
||||
|
||||
type KnowledgeBaseListReq struct {
|
||||
CreatorID *int64 `json:"creator_id"`
|
||||
FilterArgs *dto.KnowledgeBaseListFilterArgs `json:"filter_args"`
|
||||
SortArgs dto.SortArgs `json:"sort_args"`
|
||||
Pagination dto.Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
type TemplateListReq struct {
|
||||
CreatorID *int64 `json:"creator_id"`
|
||||
FilterArgs *dto.TemplateListFilterArgs `json:"filter_args"`
|
||||
SortArgs dto.SortArgs `json:"sort_args"`
|
||||
Pagination dto.Pagination `json:"pagination"`
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/config"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/auth_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/comment_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/config_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/document_service"
|
||||
svc_iface "github.com/XingfenD/yoresee_doc/internal/service/interface"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/invitation_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/knowledge_base_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/membership_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/mq_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/notification_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/setting_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/user_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/mq"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func RegisterTopicConsumer(h svc_iface.TopicConsumer) error {
|
||||
return mq_service.MQSvc.Consume(
|
||||
context.Background(),
|
||||
mq.BackendRedis,
|
||||
mq.ConsumeOptions{
|
||||
Topic: h.Topic(),
|
||||
Mode: mq.ConsumeModeFanout,
|
||||
AutoAck: true,
|
||||
OnError: mq.ErrorActionDrop,
|
||||
},
|
||||
func(ctx context.Context, message mq.Message) error {
|
||||
return h.Consume()(message.Body)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func Init(cfg *config.Config, repos *repository.Repositories) error {
|
||||
auth_service.AuthSvc = auth_service.NewAuthService(repos)
|
||||
user_service.UserSvc = user_service.NewUserService(repos)
|
||||
document_service.DocumentSvc = document_service.NewDocumentService(repos)
|
||||
comment_service.CommentSvc = comment_service.NewCommentService(repos)
|
||||
knowledge_base_service.KnowledgeBaseSvc = knowledge_base_service.NewKnowledgeBaseService(repos)
|
||||
membership_service.MembershipSvc = membership_service.NewMembershipService(repos)
|
||||
invitation_service.InvitationSvc = invitation_service.NewInvitationService(repos)
|
||||
notification_service.NotificationSvc = notification_service.NewNotificationService(repos)
|
||||
|
||||
config_service.InitConfigService()
|
||||
setting_service.InitSettingService()
|
||||
if err := InitMQTopicConsumer(); err != nil {
|
||||
logrus.Errorf("[Service layer] InitMQTopicConsumer failed, err=%+v", err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "initialize message queue topic consumer failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitMQTopicConsumer() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package svc_iface
|
||||
|
||||
type HandleFunc func(data []byte) error
|
||||
|
||||
type TopicConsumer interface {
|
||||
Topic() string
|
||||
Consume() HandleFunc
|
||||
}
|
||||
|
||||
type TopicProducer interface {
|
||||
Topic() string
|
||||
}
|
||||
|
||||
// type MQTopicHandler interface {
|
||||
// TopicProducer
|
||||
// TopicConsumer
|
||||
// }
|
||||
@@ -0,0 +1,237 @@
|
||||
package invitation_service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/invitation_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type InvitationService struct {
|
||||
invitationRepo *invitation_repo.InvitationRepository
|
||||
userRepo *user_repo.UserRepository
|
||||
}
|
||||
|
||||
func NewInvitationService(repos *repository.Repositories) *InvitationService {
|
||||
return &InvitationService{
|
||||
invitationRepo: repos.Invitation,
|
||||
userRepo: repos.User,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InvitationService) Generate(userID int64, maxUsedCnt *int64, expiresAt *time.Time, note *string) (*model.Invitation, error) {
|
||||
bytes := make([]byte, 8)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
logrus.Errorf("[Service layer: InvitationService] generate random code failed, err=%+v", err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "generate invitation code failed")
|
||||
}
|
||||
code := hex.EncodeToString(bytes)
|
||||
|
||||
invitation := &model.Invitation{
|
||||
Code: code,
|
||||
CreatedBy: userID,
|
||||
ExpiresAt: expiresAt,
|
||||
MaxUsedCnt: maxUsedCnt,
|
||||
Note: note,
|
||||
}
|
||||
|
||||
if err := s.invitationRepo.Create(invitation).Exec(); err != nil {
|
||||
return nil, status.StatusWriteDBError
|
||||
}
|
||||
return invitation, nil
|
||||
}
|
||||
|
||||
func (s *InvitationService) ListByCreator(userID int64) ([]model.Invitation, error) {
|
||||
list, err := s.invitationRepo.List(&model.Invitation{CreatedBy: userID}).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: InvitationService] ListByCreator failed, user_id=%d, err=%+v", userID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "list invitations failed")
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *InvitationService) ListInvitations(req *dto.ListInvitationsReq) ([]model.Invitation, int64, error) {
|
||||
|
||||
list, total, err := s.invitationRepo.List(&model.Invitation{}).
|
||||
WithCreatorID(req.CreatorID).
|
||||
WithKeyword(req.Keyword).
|
||||
WithMaxUsedCnt(req.MaxUsedCnt).
|
||||
WithExpiresAtRange(req.ExpiresAtStart, req.ExpiresAtEnd).
|
||||
WithCreatedAtRange(req.CreatedAtStart, req.CreatedAtEnd).
|
||||
WithDisabled(req.Disabled).
|
||||
WithSort(req.SortArgs.Field, req.SortArgs.Desc).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: InvitationService] ListInvitations failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusReadDBError, "list invitations failed")
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
func (s *InvitationService) CreateInvitation(req *dto.CreateInvitationRequest) (*model.Invitation, error) {
|
||||
if req == nil || req.CreatorExternalID == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
// TODO: normal user can't create invitation codes
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.CreatorExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusUserNotFound
|
||||
}
|
||||
return s.Generate(userID, req.MaxUsedCnt, req.ExpiresAt, req.Note)
|
||||
}
|
||||
|
||||
func (s *InvitationService) UpdateInvitation(req *dto.UpdateInvitationRequest) error {
|
||||
if req == nil || req.Code == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
return s.UpdateCode(req.Code, req.ExpiresAt, req.MaxUsedCnt, req.Disabled, req.Note)
|
||||
}
|
||||
|
||||
func (s *InvitationService) DeleteInvitation(req *dto.DeleteInvitationRequest) error {
|
||||
if req == nil || req.Code == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
inv, err := s.invitationRepo.GetByCode(req.Code).Exec()
|
||||
if err != nil {
|
||||
return status.StatusInvitationInvalid
|
||||
}
|
||||
if err := s.invitationRepo.Delete(inv.ID).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InvitationService) CreateInvitationRecord(record *model.InvitationRecord) error {
|
||||
if record == nil || record.Code == "" || record.Status == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
if record.UsedAt.IsZero() {
|
||||
record.UsedAt = time.Now()
|
||||
}
|
||||
if err := s.invitationRepo.CreateRecord(record).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InvitationService) CreateInvitationRecordWithTx(tx *gorm.DB, record *model.InvitationRecord) error {
|
||||
if record == nil || record.Code == "" || record.Status == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
if record.UsedAt.IsZero() {
|
||||
record.UsedAt = time.Now()
|
||||
}
|
||||
if err := s.invitationRepo.CreateRecord(record).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InvitationService) ListInvitationRecords(req *dto.ListInvitationRecordsRequest) ([]model.InvitationRecord, int64, error) {
|
||||
if req == nil {
|
||||
return nil, 0, status.StatusParamError
|
||||
}
|
||||
return s.invitationRepo.ListRecords().
|
||||
WithCode(req.Code).
|
||||
WithStatus(req.Status).
|
||||
WithUsedAtRange(req.UsedAtStart, req.UsedAtEnd).
|
||||
WithCreatorID(req.CreatorID).
|
||||
WithKeyword(req.Keyword).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
ExecWithTotal()
|
||||
}
|
||||
|
||||
func (s *InvitationService) ValidateAndUse(code string) error {
|
||||
ok, err := s.invitationRepo.ValidateAndUse(code).Exec()
|
||||
if err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
invitation, err := s.invitationRepo.GetByCode(code).Exec()
|
||||
if err != nil {
|
||||
return status.StatusInvitationInvalid
|
||||
}
|
||||
|
||||
if invitation.DeletedAt != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusInvitationInvalid, "invitation code deleted")
|
||||
}
|
||||
if invitation.Disabled == true {
|
||||
return status.GenErrWithCustomMsg(status.StatusInvitationInvalid, "invitation code disabled")
|
||||
}
|
||||
if invitation.ExpiresAt != nil && invitation.ExpiresAt.Before(time.Now()) {
|
||||
return status.GenErrWithCustomMsg(status.StatusInvitationInvalid, "invitation code expired")
|
||||
}
|
||||
if invitation.MaxUsedCnt != nil && invitation.UsedCnt >= *invitation.MaxUsedCnt {
|
||||
return status.GenErrWithCustomMsg(status.StatusInvitationInvalid, "invitation code used up")
|
||||
}
|
||||
|
||||
return status.StatusInvitationInvalid
|
||||
}
|
||||
|
||||
func (s *InvitationService) ValidateAndUseWithTx(tx *gorm.DB, code string) error {
|
||||
ok, err := s.invitationRepo.ValidateAndUse(code).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
invitation, err := s.invitationRepo.GetByCode(code).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusInvitationInvalid
|
||||
}
|
||||
|
||||
if invitation.DeletedAt != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusInvitationInvalid, "invitation code deleted")
|
||||
}
|
||||
if invitation.Disabled == true {
|
||||
return status.GenErrWithCustomMsg(status.StatusInvitationInvalid, "invitation code disabled")
|
||||
}
|
||||
if invitation.ExpiresAt != nil && invitation.ExpiresAt.Before(time.Now()) {
|
||||
return status.GenErrWithCustomMsg(status.StatusInvitationInvalid, "invitation code expired")
|
||||
}
|
||||
if invitation.MaxUsedCnt != nil && invitation.UsedCnt >= *invitation.MaxUsedCnt {
|
||||
return status.GenErrWithCustomMsg(status.StatusInvitationInvalid, "invitation code used up")
|
||||
}
|
||||
|
||||
return status.StatusInvitationInvalid
|
||||
}
|
||||
|
||||
func (s *InvitationService) UpdateCode(code string, newExpireTime *time.Time, newMaxUsedCnt *int64, isDisabled *bool, note *string) error {
|
||||
invitation, err := s.invitationRepo.GetByCode(code).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: InvitationService] GetByCode failed, code=%s, err=%+v", code, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusInvitationInvalid, "invitation code invalid")
|
||||
}
|
||||
if newExpireTime != nil {
|
||||
invitation.ExpiresAt = newExpireTime
|
||||
}
|
||||
if newMaxUsedCnt != nil {
|
||||
invitation.MaxUsedCnt = newMaxUsedCnt
|
||||
}
|
||||
if isDisabled != nil {
|
||||
invitation.Disabled = *isDisabled
|
||||
}
|
||||
if note != nil {
|
||||
invitation.Note = note
|
||||
}
|
||||
if err := s.invitationRepo.Update(invitation).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var InvitationSvc *InvitationService
|
||||
@@ -0,0 +1,41 @@
|
||||
package knowledge_base_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/knowledge_base_repo"
|
||||
internal_dto "github.com/XingfenD/yoresee_doc/internal/service/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func buildKnowledgeBaseListReqFromExternal(req *dto.KnowledgeBaseListByExternalRequest, creatorID *int64) *internal_dto.KnowledgeBaseListReq {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &internal_dto.KnowledgeBaseListReq{
|
||||
CreatorID: creatorID,
|
||||
FilterArgs: req.FilterArgs,
|
||||
SortArgs: req.SortArgs,
|
||||
Pagination: req.Pagination,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *KnowledgeBaseService) buildListKnowledgeBaseOperation(req *internal_dto.KnowledgeBaseListReq) (*knowledge_base_repo.ListKnowledgeBaseOperation, error) {
|
||||
if s == nil || s.knowledgeBaseRepo == nil {
|
||||
return nil, status.StatusServiceInternalError
|
||||
}
|
||||
if req == nil {
|
||||
return nil, status.StatusInternalParamsError
|
||||
}
|
||||
op := s.knowledgeBaseRepo.List(&model.KnowledgeBase{}).WithCreatorID(req.CreatorID)
|
||||
if req.FilterArgs != nil {
|
||||
op = op.WithIsPublic(req.FilterArgs.IsPublic).
|
||||
WithNameKeyword(req.FilterArgs.NameKeyword).
|
||||
WithCreateTimeRange(req.FilterArgs.CreateTimeRangeStart, req.FilterArgs.CreateTimeRangeEnd).
|
||||
WithUpdateTimeRange(req.FilterArgs.UpdateTimeRangeStart, req.FilterArgs.UpdateTimeRangeEnd)
|
||||
}
|
||||
op = op.WithSort(req.SortArgs.Field, req.SortArgs.Desc).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize)
|
||||
return op, nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package knowledge_base_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
)
|
||||
|
||||
func (s *KnowledgeBaseService) Create(req *dto.CreateKnowledgeBaseRequest) (*dto.CreateKnowledgeBaseResponse, error) {
|
||||
if req == nil || req.Name == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.CreatorExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusUserNotFound
|
||||
}
|
||||
|
||||
kbModel := &model.KnowledgeBase{
|
||||
ExternalID: utils.GenerateExternalID(utils.ExternalIDKnowledgeBase),
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Cover: req.Cover,
|
||||
IsPublic: req.IsPublic,
|
||||
CreatorUserID: userID,
|
||||
}
|
||||
|
||||
if err := s.knowledgeBaseRepo.Create(kbModel).Exec(); err != nil {
|
||||
return nil, status.StatusWriteDBError
|
||||
}
|
||||
|
||||
return &dto.CreateKnowledgeBaseResponse{ExternalID: kbModel.ExternalID}, nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package knowledge_base_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *KnowledgeBaseService) CreateRecentKnowledgeBase(req *dto.CreateRecentKnowledgeBaseRequest) error {
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
knowledgeBaseID, err := s.knowledgeBaseRepo.GetIDByExternalID(req.KnowledgeBaseExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusKnowledgeBaseNotFound
|
||||
}
|
||||
|
||||
err = s.knowledgeBaseRepo.CreateRecentKnowledgeBase(&model.RecentKnowledgeBase{
|
||||
UserID: userID,
|
||||
KnowledgeBaseID: knowledgeBaseID,
|
||||
AccessedAt: req.AssessTime,
|
||||
}).Exec()
|
||||
|
||||
if err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package knowledge_base_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type KnowledgeBaseGetByExternalIDOperation struct {
|
||||
withUserExtend bool
|
||||
withDocumentExtend bool
|
||||
req *dto.KnowledgeBaseGetByExternalIDRequest
|
||||
srvc *KnowledgeBaseService
|
||||
}
|
||||
|
||||
func (s *KnowledgeBaseService) GetByExternalID(req *dto.KnowledgeBaseGetByExternalIDRequest) *KnowledgeBaseGetByExternalIDOperation {
|
||||
return &KnowledgeBaseGetByExternalIDOperation{
|
||||
req: req,
|
||||
srvc: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (op *KnowledgeBaseGetByExternalIDOperation) WithExtend() *KnowledgeBaseGetByExternalIDOperation {
|
||||
op.withUserExtend = true
|
||||
op.withDocumentExtend = true
|
||||
return op
|
||||
}
|
||||
|
||||
func (op *KnowledgeBaseGetByExternalIDOperation) WithUserExtend() *KnowledgeBaseGetByExternalIDOperation {
|
||||
op.withUserExtend = true
|
||||
return op
|
||||
}
|
||||
|
||||
func (op *KnowledgeBaseGetByExternalIDOperation) WithDocumentExtend() *KnowledgeBaseGetByExternalIDOperation {
|
||||
op.withDocumentExtend = true
|
||||
return op
|
||||
}
|
||||
|
||||
func (op *KnowledgeBaseGetByExternalIDOperation) Exec() (*dto.KnowledgeBaseResponse, error) {
|
||||
kbModel, err := op.srvc.knowledgeBaseRepo.GetByExternalID(op.req.KnowledgeBaseExternalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: KnowledgeBaseService] GetByExternalID failed, external_id=%s, err=%+v", op.req.KnowledgeBaseExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusKnowledgeBaseNotFound, "knowledge base not found")
|
||||
}
|
||||
extendDTO := &dto.KnowledgeBaseExtend{}
|
||||
|
||||
if op.withUserExtend {
|
||||
userModel, err := op.srvc.userRepo.GetByID(kbModel.CreatorUserID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: KnowledgeBaseService] Get creator user failed, user_id=%d, err=%+v", kbModel.CreatorUserID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "query creator failed")
|
||||
}
|
||||
extendDTO.CreatorUserExternalID = userModel.ExternalID
|
||||
extendDTO.CreatorName = userModel.Username
|
||||
}
|
||||
|
||||
if op.withDocumentExtend {
|
||||
ids := []int64{
|
||||
kbModel.ID,
|
||||
}
|
||||
count, err := op.srvc.knowledgeBaseRepo.MGetKnowledgeBaseDocumentsCount(ids).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: KnowledgeBaseService] MGetKnowledgeBaseDocumentsCount failed, kb_id=%d, err=%+v", kbModel.ID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document count failed")
|
||||
}
|
||||
extendDTO.DocumentsCount = count[kbModel.ID]
|
||||
}
|
||||
|
||||
return dto.NewKnowledgeBaseResponseFromModel(
|
||||
kbModel, extendDTO,
|
||||
), nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package knowledge_base_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/knowledge_base_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
type KnowledgeBaseService struct {
|
||||
knowledgeBaseRepo *knowledge_base_repo.KnowledgeBaseRepository
|
||||
userRepo *user_repo.UserRepository
|
||||
}
|
||||
|
||||
func NewKnowledgeBaseService(repos *repository.Repositories) *KnowledgeBaseService {
|
||||
return &KnowledgeBaseService{
|
||||
knowledgeBaseRepo: repos.KnowledgeBase,
|
||||
userRepo: repos.User,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *KnowledgeBaseService) GetIDByExternalID(externalID string) (int64, error) {
|
||||
id, err := s.knowledgeBaseRepo.GetIDByExternalID(externalID).Exec()
|
||||
if err != nil {
|
||||
return 0, status.StatusKnowledgeBaseNotFound
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
var KnowledgeBaseSvc *KnowledgeBaseService
|
||||
@@ -0,0 +1,142 @@
|
||||
package knowledge_base_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
internal_dto "github.com/XingfenD/yoresee_doc/internal/service/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/bytedance/gg/gslice"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type knowledgeBaseListOperation struct {
|
||||
req *internal_dto.KnowledgeBaseListReq
|
||||
srvc *KnowledgeBaseService
|
||||
|
||||
withDocumentExtend bool
|
||||
withUserExtend bool
|
||||
}
|
||||
|
||||
func (s *KnowledgeBaseService) list(req *internal_dto.KnowledgeBaseListReq) *knowledgeBaseListOperation {
|
||||
return &knowledgeBaseListOperation{
|
||||
req: req,
|
||||
srvc: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (op *knowledgeBaseListOperation) WithDocumentExtend() *knowledgeBaseListOperation {
|
||||
op.withDocumentExtend = true
|
||||
return op
|
||||
}
|
||||
|
||||
func (op *knowledgeBaseListOperation) WithUserExtend() *knowledgeBaseListOperation {
|
||||
op.withUserExtend = true
|
||||
return op
|
||||
}
|
||||
|
||||
func (op *knowledgeBaseListOperation) documentExtend(kbModels []*model.KnowledgeBase, kbExtendMapByID map[int64]*dto.KnowledgeBaseExtend) error {
|
||||
if op.withDocumentExtend {
|
||||
kbIDs := gslice.Map(kbModels, func(kbModel *model.KnowledgeBase) int64 {
|
||||
return kbModel.ID
|
||||
})
|
||||
countMapByKbID, err := op.srvc.knowledgeBaseRepo.MGetKnowledgeBaseDocumentsCount(kbIDs).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: knowledgeBaseList]: MGetKnowledgeBaseDocumentsCount failed, err: %+v", err)
|
||||
return status.GenErrWithCustomMsg(status.StatusReadDBError, "query knowledge base document count failed")
|
||||
}
|
||||
for id, count := range countMapByKbID {
|
||||
if _, ok := kbExtendMapByID[id]; !ok {
|
||||
kbExtendMapByID[id] = &dto.KnowledgeBaseExtend{}
|
||||
}
|
||||
kbExtendMapByID[id].DocumentsCount = count
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (op *knowledgeBaseListOperation) userExtend(kbModels []*model.KnowledgeBase, kbExtendMapByID map[int64]*dto.KnowledgeBaseExtend) error {
|
||||
if op.withUserExtend {
|
||||
// collect all user_id
|
||||
allUserID := gslice.Map(kbModels, func(kb *model.KnowledgeBase) int64 {
|
||||
return kb.CreatorUserID
|
||||
})
|
||||
uniqUserID := gslice.Uniq(allUserID)
|
||||
usersMapByUserID, err := op.srvc.userRepo.MGetUserByID(uniqUserID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: knowledgeBaseList]: MGetUserByID failed, err: %+v", err)
|
||||
return status.GenErrWithCustomMsg(status.StatusReadDBError, "query creator users failed")
|
||||
}
|
||||
for _, kbModel := range kbModels {
|
||||
if _, ok := kbExtendMapByID[kbModel.ID]; !ok {
|
||||
kbExtendMapByID[kbModel.ID] = &dto.KnowledgeBaseExtend{}
|
||||
}
|
||||
if user, ok := usersMapByUserID[kbModel.CreatorUserID]; ok {
|
||||
kbExtendMapByID[kbModel.ID].CreatorUserExternalID = user.ExternalID
|
||||
kbExtendMapByID[kbModel.ID].CreatorName = user.Username
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (op *knowledgeBaseListOperation) Exec() ([]*dto.KnowledgeBaseResponse, error) {
|
||||
listOp, err := op.srvc.buildListKnowledgeBaseOperation(op.req)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: knowledgeBaseList]: buildListKnowledgeBaseOperation failed, err: %+v", err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "build knowledge base list operation failed")
|
||||
}
|
||||
kbModels, err := listOp.Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: knowledgeBaseList]: list operation exec failed, err: %+v", err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "list knowledge bases failed")
|
||||
}
|
||||
|
||||
kbExtendMapByID := make(map[int64]*dto.KnowledgeBaseExtend)
|
||||
|
||||
if err := op.documentExtend(kbModels, kbExtendMapByID); err != nil {
|
||||
logrus.Errorf("[Service layer: knowledgeBaseList]: documentExtend failed, err: %+v", err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "build knowledge base document extension failed")
|
||||
}
|
||||
if err := op.userExtend(kbModels, kbExtendMapByID); err != nil {
|
||||
logrus.Errorf("[Service layer: knowledgeBaseList]: userExtend failed, err: %+v", err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "build knowledge base user extension failed")
|
||||
}
|
||||
|
||||
knowledgeBases := make([]*dto.KnowledgeBaseResponse, 0, len(kbModels))
|
||||
for _, kb := range kbModels {
|
||||
knowledgeBases = append(knowledgeBases, dto.NewKnowledgeBaseResponseFromModel(kb, kbExtendMapByID[kb.ID]))
|
||||
}
|
||||
|
||||
return knowledgeBases, nil
|
||||
}
|
||||
|
||||
func (op *knowledgeBaseListOperation) ExecWithTotal() ([]*dto.KnowledgeBaseResponse, int64, error) {
|
||||
listOp, err := op.srvc.buildListKnowledgeBaseOperation(op.req)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: knowledgeBaseList]: buildListKnowledgeBaseOperation failed, err: %+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build knowledge base list operation failed")
|
||||
}
|
||||
kbModels, total, err := listOp.ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: knowledgeBaseList]: list operation exec with total failed, err: %+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "list knowledge bases failed")
|
||||
}
|
||||
|
||||
kbExtendMapByID := make(map[int64]*dto.KnowledgeBaseExtend)
|
||||
|
||||
if err := op.documentExtend(kbModels, kbExtendMapByID); err != nil {
|
||||
logrus.Errorf("[Service layer: knowledgeBaseList]: documentExtend failed, err: %+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build knowledge base document extension failed")
|
||||
}
|
||||
if err := op.userExtend(kbModels, kbExtendMapByID); err != nil {
|
||||
logrus.Errorf("[Service layer: knowledgeBaseList]: userExtend failed, err: %+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build knowledge base user extension failed")
|
||||
}
|
||||
|
||||
knowledgeBases := make([]*dto.KnowledgeBaseResponse, 0, len(kbModels))
|
||||
for _, kb := range kbModels {
|
||||
knowledgeBases = append(knowledgeBases, dto.NewKnowledgeBaseResponseFromModel(kb, kbExtendMapByID[kb.ID]))
|
||||
}
|
||||
|
||||
return knowledgeBases, total, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package knowledge_base_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *KnowledgeBaseService) ListByExternal(req *dto.KnowledgeBaseListByExternalRequest) ([]*dto.KnowledgeBaseResponse, int64, error) {
|
||||
var creatorID *int64
|
||||
if req.CreatorExternalID != "" {
|
||||
id, err := s.userRepo.GetIDByExternalID(req.CreatorExternalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: KnowledgeBaseService] GetIDByExternalID failed, creator_external_id=%s, err=%+v", req.CreatorExternalID, err)
|
||||
return nil, 0, status.StatusUserNotFound
|
||||
}
|
||||
creatorID = &id
|
||||
}
|
||||
|
||||
routerReq := buildKnowledgeBaseListReqFromExternal(req, creatorID)
|
||||
list, total, err := s.list(routerReq).WithDocumentExtend().WithUserExtend().ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: KnowledgeBaseService] ListByExternal failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "list knowledge bases failed")
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package knowledge_base_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *KnowledgeBaseService) ListRecentKnowledgeBases(req *dto.ListRecentKnowledgeBasesRequest) ([]*dto.KnowledgeBaseResponse, int64, error) {
|
||||
if req == nil || req.UserExternalID == "" {
|
||||
return nil, 0, status.StatusInternalParamsError
|
||||
}
|
||||
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusUserNotFound
|
||||
}
|
||||
|
||||
recentRecords, total, err := s.knowledgeBaseRepo.ListRecentKnowledgeBases(userID).
|
||||
WithTimeRange(req.StartTime, req.EndTime).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
if len(recentRecords) == 0 {
|
||||
return []*dto.KnowledgeBaseResponse{}, total, nil
|
||||
}
|
||||
|
||||
kbIDs := make([]int64, 0, len(recentRecords))
|
||||
for _, r := range recentRecords {
|
||||
kbIDs = append(kbIDs, r.KnowledgeBaseID)
|
||||
}
|
||||
|
||||
kbModels, err := s.knowledgeBaseRepo.MGetKnowledgeBaseByIDs(kbIDs).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
kbMap := make(map[int64]*model.KnowledgeBase, len(kbModels))
|
||||
for _, kb := range kbModels {
|
||||
kbMap[kb.ID] = kb
|
||||
}
|
||||
|
||||
kbExtendMapByID := make(map[int64]*dto.KnowledgeBaseExtend)
|
||||
op := &knowledgeBaseListOperation{
|
||||
srvc: s,
|
||||
withDocumentExtend: true,
|
||||
withUserExtend: true,
|
||||
}
|
||||
_ = op.documentExtend(kbModels, kbExtendMapByID)
|
||||
_ = op.userExtend(kbModels, kbExtendMapByID)
|
||||
|
||||
resp := make([]*dto.KnowledgeBaseResponse, 0, len(recentRecords))
|
||||
for _, record := range recentRecords {
|
||||
kb := kbMap[record.KnowledgeBaseID]
|
||||
if kb == nil {
|
||||
continue
|
||||
}
|
||||
resp = append(resp, dto.NewKnowledgeBaseResponseFromModel(kb, kbExtendMapByID[kb.ID]))
|
||||
}
|
||||
|
||||
return resp, total, nil
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package membership_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/membership_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MembershipService struct {
|
||||
db *gorm.DB
|
||||
repo *membership_repo.MembershipRepository
|
||||
useRepo *user_repo.UserRepository
|
||||
}
|
||||
|
||||
func NewMembershipService(repos *repository.Repositories) *MembershipService {
|
||||
return &MembershipService{
|
||||
db: repos.DB,
|
||||
repo: repos.Membership,
|
||||
useRepo: repos.User,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MembershipService) GetMembershipIDByExternalID(req *dto.MembershipBaseRequest) (int64, error) {
|
||||
var membershipID int64
|
||||
if req.Type == dto.MembershipType_OrgNode {
|
||||
orgNodeID, err := s.repo.GetOrgNodeIDByExternalID(req.MembershipExternalID).Exec()
|
||||
if err != nil {
|
||||
return 0, status.StatusMembershipMetaNotFound
|
||||
}
|
||||
membershipID = orgNodeID
|
||||
} else if req.Type == dto.MembershipType_UserGroup {
|
||||
userGroupID, err := s.repo.GetUserGroupIDByExternalID(req.MembershipExternalID).Exec()
|
||||
if err != nil {
|
||||
return 0, status.StatusMembershipMetaNotFound
|
||||
}
|
||||
membershipID = userGroupID
|
||||
} else {
|
||||
return 0, status.StatusInvalidMembershipType
|
||||
}
|
||||
return membershipID, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) CreateMembershipRelation(membership *dto.CreateMembershipRelationRequest) error {
|
||||
userID, err := s.useRepo.GetIDByExternalID(membership.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] GetIDByExternalID failed, user_external_id=%s, err=%+v", membership.UserExternalID, err)
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
membershipID, err := s.GetMembershipIDByExternalID(&membership.MembershipBaseRequest)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] GetMembershipIDByExternalID failed, membership_external_id=%s, type=%d, err=%+v", membership.MembershipExternalID, membership.Type, err)
|
||||
return status.GenErrWithCustomMsg(err, "membership target not found")
|
||||
}
|
||||
model := &model.MembershipRelation{
|
||||
UserID: userID,
|
||||
MembershipID: membershipID,
|
||||
Type: model.MembershipType(membership.Type),
|
||||
}
|
||||
|
||||
if err := s.repo.CreateMembership(model).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] CreateMembership failed, user_id=%d, membership_id=%d, type=%d, err=%+v", userID, membershipID, membership.Type, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create membership relation failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) GetUserGroupMeta(externalID string) (*model.UserGroupMeta, error) {
|
||||
meta, err := s.repo.GetUserGroupByExternalID(externalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] GetUserGroupMeta failed, external_id=%s, err=%+v", externalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusMembershipMetaNotFound, "user group not found")
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) GetOrgNodeMeta(externalID string) (*model.OrgNodeMeta, error) {
|
||||
meta, err := s.repo.GetOrgNodeByExternalID(externalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] GetOrgNodeMeta failed, external_id=%s, err=%+v", externalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusMembershipMetaNotFound, "org node not found")
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
// func (s *MembershipService) GetMembershipRelation(req *dto.MembershipBaseRequest) (*dto.MembershipRelationResponse, error) {
|
||||
// var membershipMetaResponse *dto.MembershipMetaResponse
|
||||
// var membershipID int64
|
||||
// switch req.Type {
|
||||
// case model.MembershipType_UserGroup:
|
||||
// userGroupMeta, err := s.GetUserGroupMeta(req.MembershipExternalID)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// membershipMetaResponse = dto.NewMembershipMetaResponseFromUserGroupMetaModel(userGroupMeta)
|
||||
// membershipID = userGroupMeta.ID
|
||||
// case model.MembershipType_OrgNode:
|
||||
// orgNodeMeta, err := s.GetOrgNodeMeta(req.MembershipExternalID)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// membershipMetaResponse = dto.NewMembershipMetaResponseFromOrgNodeMetaModel(orgNodeMeta)
|
||||
// membershipID = orgNodeMeta.ID
|
||||
// default:
|
||||
// return nil, status.StatusInvalidMembershipType
|
||||
// }
|
||||
// model := &model.MembershipRelation{
|
||||
// Type: req.Type,
|
||||
// MembershipID: membershipID,
|
||||
// }
|
||||
// memberships, err := s.repo.ListMembership(model).Exec()
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// }
|
||||
|
||||
var MembershipSvc *MembershipService
|
||||
@@ -0,0 +1,473 @@
|
||||
package membership_service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/bytedance/gg/gslice"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *MembershipService) ListOrgNodes(req *dto.ListOrgNodesRequest) ([]*dto.OrgNodeResponse, int64, error) {
|
||||
if req == nil {
|
||||
return nil, 0, status.StatusParamError
|
||||
}
|
||||
req.Pagination = normalizePagination(req.Pagination)
|
||||
|
||||
var parentID int64 = 0
|
||||
if req.ParentExternalID != "" {
|
||||
parent, err := s.repo.GetOrgNodeByExternalID(req.ParentExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusMembershipMetaNotFound
|
||||
}
|
||||
parentID = parent.ID
|
||||
}
|
||||
|
||||
nodes, total, err := s.repo.QueryOrgNode().
|
||||
WithParentID(parentID).
|
||||
WithKeyword(req.Keyword).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
ExecWithTotal()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
responses, err := s.buildOrgNodeResponses(nodes, req.IncludeChildren)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] buildOrgNodeResponses failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build org node response failed")
|
||||
}
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) GetOrgNode(req *dto.GetOrgNodeRequest) (*dto.OrgNodeResponse, error) {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
node, err := s.repo.GetOrgNodeByExternalID(req.ExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusMembershipMetaNotFound
|
||||
}
|
||||
|
||||
if req.IncludeChildren {
|
||||
return s.buildOrgNodeWithChildren(node)
|
||||
}
|
||||
|
||||
responses, err := s.buildOrgNodeResponses([]model.OrgNodeMeta{*node}, false)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] buildOrgNodeResponses failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "build org node response failed")
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
return nil, status.StatusMembershipMetaNotFound
|
||||
}
|
||||
return responses[0], nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) CreateOrgNode(req *dto.CreateOrgNodeRequest) (string, error) {
|
||||
if req == nil || strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.CreatorUserExternalID) == "" {
|
||||
return "", status.StatusParamError
|
||||
}
|
||||
|
||||
var createdExternalID string
|
||||
err := utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
creatorID, err := s.useRepo.GetIDByExternalID(req.CreatorUserExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
var parentID int64 = 0
|
||||
var parentPath string = ""
|
||||
if req.ParentExternalID != "" {
|
||||
parent, err := s.repo.GetOrgNodeByExternalID(req.ParentExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusMembershipMetaNotFound
|
||||
}
|
||||
parentID = parent.ID
|
||||
parentPath = parent.Path
|
||||
}
|
||||
|
||||
node := &model.OrgNodeMeta{
|
||||
ExternalID: utils.GenerateExternalID(utils.ExternalIDContextOrgNode),
|
||||
ParentID: parentID,
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Description: strings.TrimSpace(req.Description),
|
||||
CreatorID: creatorID,
|
||||
}
|
||||
|
||||
if err := s.repo.CreateOrgNode(node).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
|
||||
node.Path = buildOrgNodePath(parentPath, node.ID)
|
||||
if err := s.repo.UpdateOrgNode(node).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
|
||||
userIDs, err := s.resolveUserExternalIDsToIDs(req.MemberUserExternalIDs, tx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] resolveUserExternalIDsToIDs failed, err=%+v", err)
|
||||
return status.GenErrWithCustomMsg(err, "resolve user ids failed")
|
||||
}
|
||||
if err := s.syncOrgNodeMembers(tx, node.ID, userIDs); err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] syncOrgNodeMembers failed, node_id=%d, err=%+v", node.ID, err)
|
||||
return status.GenErrWithCustomMsg(err, "sync org node members failed")
|
||||
}
|
||||
|
||||
createdExternalID = node.ExternalID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] CreateOrgNode transaction failed, err=%+v", err)
|
||||
return "", status.GenErrWithCustomMsg(err, "create org node failed")
|
||||
}
|
||||
return createdExternalID, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) UpdateOrgNode(req *dto.UpdateOrgNodeRequest) error {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
if req.Name == nil && req.Description == nil && !req.SyncMembers {
|
||||
return status.StatusParamError
|
||||
}
|
||||
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
node, err := s.repo.GetOrgNodeByExternalID(req.ExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusMembershipMetaNotFound
|
||||
}
|
||||
|
||||
shouldSave := false
|
||||
if req.Name != nil {
|
||||
newName := strings.TrimSpace(*req.Name)
|
||||
if newName == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
node.Name = newName
|
||||
shouldSave = true
|
||||
}
|
||||
if req.Description != nil {
|
||||
node.Description = strings.TrimSpace(*req.Description)
|
||||
shouldSave = true
|
||||
}
|
||||
if shouldSave {
|
||||
if err := s.repo.UpdateOrgNode(node).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
}
|
||||
|
||||
if req.SyncMembers {
|
||||
userIDs, err := s.resolveUserExternalIDsToIDs(req.MemberUserExternalIDs, tx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] resolveUserExternalIDsToIDs failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return status.GenErrWithCustomMsg(err, "resolve user ids failed")
|
||||
}
|
||||
if err := s.syncOrgNodeMembers(tx, node.ID, userIDs); err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] syncOrgNodeMembers failed, node_id=%d, err=%+v", node.ID, err)
|
||||
return status.GenErrWithCustomMsg(err, "sync org node members failed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MembershipService) DeleteOrgNode(req *dto.DeleteOrgNodeRequest) error {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
node, err := s.repo.GetOrgNodeByExternalID(req.ExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusMembershipMetaNotFound
|
||||
}
|
||||
|
||||
children, err := s.repo.QueryOrgNode().WithParentID(node.ID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusReadDBError
|
||||
}
|
||||
if len(children) > 0 {
|
||||
return status.StatusOrgNodeHasChildren
|
||||
}
|
||||
|
||||
if err := tx.Where("type = ? AND membership_id = ?", model.MembershipType_OrgNode, node.ID).
|
||||
Delete(&model.MembershipRelation{}).Error; err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
if err := s.repo.DeleteOrgNode(node).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MembershipService) MoveOrgNode(req *dto.MoveOrgNodeRequest) error {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
node, err := s.repo.GetOrgNodeByExternalID(req.ExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusMembershipMetaNotFound
|
||||
}
|
||||
|
||||
var newParentID int64 = 0
|
||||
var newParentPath string = ""
|
||||
if req.NewParentExternalID != "" {
|
||||
newParent, err := s.repo.GetOrgNodeByExternalID(req.NewParentExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusMembershipMetaNotFound
|
||||
}
|
||||
newParentID = newParent.ID
|
||||
newParentPath = newParent.Path
|
||||
|
||||
if newParentID == node.ID {
|
||||
return status.StatusParamError
|
||||
}
|
||||
|
||||
descendants, err := s.repo.QueryOrgNodeByPathPrefix(node.Path).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusReadDBError
|
||||
}
|
||||
for _, descendant := range descendants {
|
||||
if descendant.ID == newParentID {
|
||||
return status.StatusOrgNodeCannotMoveToDescendant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.ParentID = newParentID
|
||||
oldPath := node.Path
|
||||
node.Path = buildOrgNodePath(newParentPath, node.ID)
|
||||
|
||||
if err := s.repo.UpdateOrgNode(node).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
|
||||
descendants, err := s.repo.QueryOrgNodeByPathPrefix(oldPath).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusReadDBError
|
||||
}
|
||||
for _, descendant := range descendants {
|
||||
if descendant.ID == node.ID {
|
||||
continue
|
||||
}
|
||||
descendant.Path = strings.Replace(descendant.Path, oldPath, node.Path, 1)
|
||||
if err := s.repo.UpdateOrgNode(&descendant).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MembershipService) ListOrgNodeMembers(req *dto.ListOrgNodeMembersRequest) ([]*dto.UserResponse, int64, error) {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return nil, 0, status.StatusParamError
|
||||
}
|
||||
req.Pagination = normalizePagination(req.Pagination)
|
||||
|
||||
node, err := s.repo.GetOrgNodeByExternalID(req.ExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusMembershipMetaNotFound
|
||||
}
|
||||
|
||||
relations, err := s.repo.ListMembership(&model.MembershipRelation{
|
||||
Type: model.MembershipType_OrgNode,
|
||||
MembershipID: node.ID,
|
||||
}).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
if len(relations) == 0 {
|
||||
return []*dto.UserResponse{}, 0, nil
|
||||
}
|
||||
|
||||
memberIDs := make([]int64, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
memberIDs = append(memberIDs, relation.UserID)
|
||||
}
|
||||
|
||||
query := s.useRepo.QueryUsers().
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize)
|
||||
if req.Keyword != nil && strings.TrimSpace(*req.Keyword) != "" {
|
||||
query = query.WithKeyword(req.Keyword)
|
||||
}
|
||||
query = query.WithUserIDs(memberIDs)
|
||||
|
||||
users, total, err := query.ExecWithTotal()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
resp := make([]*dto.UserResponse, 0, len(users))
|
||||
for i := range users {
|
||||
user := users[i]
|
||||
resp = append(resp, dto.NewUserResponseFromModel(&user))
|
||||
}
|
||||
|
||||
return resp, total, nil
|
||||
}
|
||||
|
||||
func buildOrgNodePath(parentPath string, nodeID int64) string {
|
||||
label := "n" + utils.Int64ToString(nodeID)
|
||||
if strings.TrimSpace(parentPath) == "" {
|
||||
return label
|
||||
}
|
||||
return parentPath + "." + label
|
||||
}
|
||||
|
||||
func (s *MembershipService) buildOrgNodeResponses(nodes []model.OrgNodeMeta, includeChildren bool) ([]*dto.OrgNodeResponse, error) {
|
||||
if len(nodes) == 0 {
|
||||
return []*dto.OrgNodeResponse{}, nil
|
||||
}
|
||||
|
||||
nodeIDs := make([]int64, 0, len(nodes))
|
||||
parentIDs := make([]int64, 0, len(nodes))
|
||||
creatorIDs := make([]int64, 0, len(nodes))
|
||||
for _, n := range nodes {
|
||||
nodeIDs = append(nodeIDs, n.ID)
|
||||
parentIDs = append(parentIDs, n.ParentID)
|
||||
creatorIDs = append(creatorIDs, n.CreatorID)
|
||||
}
|
||||
parentIDs = gslice.Uniq(parentIDs)
|
||||
creatorIDs = gslice.Uniq(creatorIDs)
|
||||
|
||||
relations, err := s.repo.ListMembershipByTypeAndIDs(model.MembershipType_OrgNode, nodeIDs).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
|
||||
parentMap, err := s.repo.MGetOrgNodeByID(parentIDs).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
|
||||
creatorMap, err := s.useRepo.MGetUserByID(creatorIDs).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
|
||||
nodeMemberUserIDs := make(map[int64][]int64, len(nodeIDs))
|
||||
nodeMemberSeen := make(map[int64]map[int64]struct{}, len(nodeIDs))
|
||||
for _, relation := range relations {
|
||||
if _, ok := nodeMemberSeen[relation.MembershipID]; !ok {
|
||||
nodeMemberSeen[relation.MembershipID] = map[int64]struct{}{}
|
||||
}
|
||||
if _, exists := nodeMemberSeen[relation.MembershipID][relation.UserID]; exists {
|
||||
continue
|
||||
}
|
||||
nodeMemberSeen[relation.MembershipID][relation.UserID] = struct{}{}
|
||||
|
||||
nodeMemberUserIDs[relation.MembershipID] = append(nodeMemberUserIDs[relation.MembershipID], relation.UserID)
|
||||
}
|
||||
|
||||
responses := make([]*dto.OrgNodeResponse, 0, len(nodes))
|
||||
for _, n := range nodes {
|
||||
resp := &dto.OrgNodeResponse{
|
||||
ExternalID: n.ExternalID,
|
||||
Name: n.Name,
|
||||
Path: n.Path,
|
||||
Description: n.Description,
|
||||
MemberCount: len(nodeMemberUserIDs[n.ID]),
|
||||
}
|
||||
if parent := parentMap[n.ParentID]; parent != nil {
|
||||
resp.ParentExternalID = parent.ExternalID
|
||||
}
|
||||
if creator := creatorMap[n.CreatorID]; creator != nil {
|
||||
resp.CreatorUserExternalID = creator.ExternalID
|
||||
}
|
||||
responses = append(responses, resp)
|
||||
}
|
||||
|
||||
if includeChildren {
|
||||
for i, resp := range responses {
|
||||
node := nodes[i]
|
||||
children, err := s.repo.QueryOrgNode().WithParentID(node.ID).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
if len(children) > 0 {
|
||||
childResponses, err := s.buildOrgNodeResponses(children, true)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] buildOrgNodeResponses children failed, node_id=%d, err=%+v", node.ID, err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "build org node children response failed")
|
||||
}
|
||||
resp.Children = childResponses
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) buildOrgNodeWithChildren(node *model.OrgNodeMeta) (*dto.OrgNodeResponse, error) {
|
||||
responses, err := s.buildOrgNodeResponses([]model.OrgNodeMeta{*node}, true)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] buildOrgNodeResponses tree failed, node_external_id=%s, err=%+v", node.ExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "build org node tree failed")
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
return nil, status.StatusMembershipMetaNotFound
|
||||
}
|
||||
return responses[0], nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) syncOrgNodeMembers(tx *gorm.DB, orgNodeID int64, targetUserIDs []int64) error {
|
||||
existingRelations, err := s.repo.ListMembership(&model.MembershipRelation{
|
||||
Type: model.MembershipType_OrgNode,
|
||||
MembershipID: orgNodeID,
|
||||
}).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusReadDBError
|
||||
}
|
||||
|
||||
existingUserIDs := map[int64]struct{}{}
|
||||
for _, relation := range existingRelations {
|
||||
existingUserIDs[relation.UserID] = struct{}{}
|
||||
}
|
||||
targetUserIDSet := map[int64]struct{}{}
|
||||
for _, userID := range targetUserIDs {
|
||||
targetUserIDSet[userID] = struct{}{}
|
||||
}
|
||||
|
||||
toCreate := make([]*model.MembershipRelation, 0)
|
||||
for userID := range targetUserIDSet {
|
||||
if _, ok := existingUserIDs[userID]; ok {
|
||||
continue
|
||||
}
|
||||
toCreate = append(toCreate, &model.MembershipRelation{
|
||||
Type: model.MembershipType_OrgNode,
|
||||
UserID: userID,
|
||||
MembershipID: orgNodeID,
|
||||
})
|
||||
}
|
||||
|
||||
toDeleteUserIDs := make([]int64, 0)
|
||||
for userID := range existingUserIDs {
|
||||
if _, ok := targetUserIDSet[userID]; ok {
|
||||
continue
|
||||
}
|
||||
toDeleteUserIDs = append(toDeleteUserIDs, userID)
|
||||
}
|
||||
|
||||
if len(toCreate) > 0 {
|
||||
if err := s.repo.BatchCreateMembership(toCreate).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
}
|
||||
if len(toDeleteUserIDs) > 0 {
|
||||
if err := tx.Where("type = ? AND membership_id = ? AND user_id IN ?", model.MembershipType_OrgNode, orgNodeID, toDeleteUserIDs).
|
||||
Delete(&model.MembershipRelation{}).Error; err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
package membership_service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/auth"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/bytedance/gg/gslice"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPage = 1
|
||||
defaultPageSize = 20
|
||||
maxPageSize = 200
|
||||
)
|
||||
|
||||
func normalizePagination(p dto.Pagination) dto.Pagination {
|
||||
if p.Page <= 0 {
|
||||
p.Page = defaultPage
|
||||
}
|
||||
if p.PageSize <= 0 {
|
||||
p.PageSize = defaultPageSize
|
||||
}
|
||||
if p.PageSize > maxPageSize {
|
||||
p.PageSize = maxPageSize
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *MembershipService) ListUsers(req *dto.ListUsersRequest) ([]*dto.UserResponse, int64, error) {
|
||||
if req == nil {
|
||||
return nil, 0, status.StatusParamError
|
||||
}
|
||||
req.Pagination = normalizePagination(req.Pagination)
|
||||
|
||||
users, total, err := s.useRepo.QueryUsers().
|
||||
WithKeyword(req.Keyword).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
ExecWithTotal()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
resp := make([]*dto.UserResponse, 0, len(users))
|
||||
for i := range users {
|
||||
user := users[i]
|
||||
resp = append(resp, dto.NewUserResponseFromModel(&user))
|
||||
}
|
||||
return resp, total, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) UpdateUser(req *dto.UpdateUserRequest) error {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
if req.Username == nil && req.Email == nil && req.Nickname == nil && req.Status == nil {
|
||||
return status.StatusParamError
|
||||
}
|
||||
|
||||
user, err := s.useRepo.GetByExternalID(req.ExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
oldStatus := user.Status
|
||||
|
||||
if req.Username != nil {
|
||||
username := strings.TrimSpace(*req.Username)
|
||||
if username == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
user.Username = username
|
||||
}
|
||||
if req.Email != nil {
|
||||
email := strings.TrimSpace(*req.Email)
|
||||
if email == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
user.Email = email
|
||||
}
|
||||
if req.Nickname != nil {
|
||||
user.Nickname = strings.TrimSpace(*req.Nickname)
|
||||
}
|
||||
if req.Status != nil {
|
||||
user.Status = int(*req.Status)
|
||||
}
|
||||
|
||||
if err := s.useRepo.Update(user).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
if req.Status != nil && oldStatus > 0 && user.Status <= 0 {
|
||||
if err := auth.BlacklistUserJWTs(user.ExternalID); err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] blacklist user jwt tokens failed, user_external_id=%s, err=%+v", user.ExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "ban user failed: blacklist jwt tokens failed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) ListUserGroupMembers(req *dto.ListUserGroupMembersRequest) ([]*dto.UserResponse, int64, error) {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return nil, 0, status.StatusParamError
|
||||
}
|
||||
req.Pagination = normalizePagination(req.Pagination)
|
||||
|
||||
group, err := s.repo.GetUserGroupByExternalID(req.ExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusMembershipMetaNotFound
|
||||
}
|
||||
|
||||
relations, err := s.repo.ListMembership(&model.MembershipRelation{
|
||||
Type: model.MembershipType_UserGroup,
|
||||
MembershipID: group.ID,
|
||||
}).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
if len(relations) == 0 {
|
||||
return []*dto.UserResponse{}, 0, nil
|
||||
}
|
||||
|
||||
memberIDs := make([]int64, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
memberIDs = append(memberIDs, relation.UserID)
|
||||
}
|
||||
|
||||
query := s.useRepo.QueryUsers().
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize)
|
||||
if req.Keyword != nil && strings.TrimSpace(*req.Keyword) != "" {
|
||||
query = query.WithKeyword(req.Keyword)
|
||||
}
|
||||
query = query.WithUserIDs(memberIDs)
|
||||
|
||||
users, total, err := query.ExecWithTotal()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
resp := make([]*dto.UserResponse, 0, len(users))
|
||||
for i := range users {
|
||||
user := users[i]
|
||||
resp = append(resp, dto.NewUserResponseFromModel(&user))
|
||||
}
|
||||
|
||||
return resp, total, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) ListUserGroups(req *dto.ListUserGroupsRequest) ([]*dto.UserGroupResponse, int64, error) {
|
||||
if req == nil {
|
||||
return nil, 0, status.StatusParamError
|
||||
}
|
||||
req.Pagination = normalizePagination(req.Pagination)
|
||||
|
||||
groups, total, err := s.repo.QueryUserGroup().
|
||||
WithKeyword(req.Keyword).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
ExecWithTotal()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
responses, err := s.buildUserGroupResponses(groups, false)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] buildUserGroupResponses failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build user group response failed")
|
||||
}
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) GetUserGroup(req *dto.GetUserGroupRequest) (*dto.UserGroupResponse, error) {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
group, err := s.repo.GetUserGroupByExternalID(req.ExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusMembershipMetaNotFound
|
||||
}
|
||||
|
||||
responses, err := s.buildUserGroupResponses([]model.UserGroupMeta{*group}, false)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] buildUserGroupResponses failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "build user group response failed")
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
return nil, status.StatusMembershipMetaNotFound
|
||||
}
|
||||
return responses[0], nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) CreateUserGroup(req *dto.CreateUserGroupRequest) (string, error) {
|
||||
if req == nil || strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.CreatorUserExternalID) == "" {
|
||||
return "", status.StatusParamError
|
||||
}
|
||||
|
||||
var createdExternalID string
|
||||
err := utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
creatorID, err := s.useRepo.GetIDByExternalID(req.CreatorUserExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
group := &model.UserGroupMeta{
|
||||
ExternalID: utils.GenerateExternalID(utils.ExternalIDContextUserGroup),
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Description: strings.TrimSpace(req.Description),
|
||||
CreatorID: creatorID,
|
||||
}
|
||||
if err := s.repo.CreateUserGroup(group).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
|
||||
userIDs, err := s.resolveUserExternalIDsToIDs(req.MemberUserExternalIDs, tx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] resolveUserExternalIDsToIDs failed, err=%+v", err)
|
||||
return status.GenErrWithCustomMsg(err, "resolve user ids failed")
|
||||
}
|
||||
if err := s.syncUserGroupMembers(tx, group.ID, userIDs); err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] syncUserGroupMembers failed, group_id=%d, err=%+v", group.ID, err)
|
||||
return status.GenErrWithCustomMsg(err, "sync user group members failed")
|
||||
}
|
||||
|
||||
createdExternalID = group.ExternalID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] CreateUserGroup transaction failed, err=%+v", err)
|
||||
return "", status.GenErrWithCustomMsg(err, "create user group failed")
|
||||
}
|
||||
return createdExternalID, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) UpdateUserGroup(req *dto.UpdateUserGroupRequest) error {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
if req.Name == nil && req.Description == nil && !req.SyncMembers {
|
||||
return status.StatusParamError
|
||||
}
|
||||
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
group, err := s.repo.GetUserGroupByExternalID(req.ExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusMembershipMetaNotFound
|
||||
}
|
||||
|
||||
shouldSave := false
|
||||
if req.Name != nil {
|
||||
newName := strings.TrimSpace(*req.Name)
|
||||
if newName == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
group.Name = newName
|
||||
shouldSave = true
|
||||
}
|
||||
if req.Description != nil {
|
||||
group.Description = strings.TrimSpace(*req.Description)
|
||||
shouldSave = true
|
||||
}
|
||||
if shouldSave {
|
||||
if err := s.repo.UpdateUserGroup(group).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
}
|
||||
|
||||
if req.SyncMembers {
|
||||
userIDs, err := s.resolveUserExternalIDsToIDs(req.MemberUserExternalIDs, tx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] resolveUserExternalIDsToIDs failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return status.GenErrWithCustomMsg(err, "resolve user ids failed")
|
||||
}
|
||||
if err := s.syncUserGroupMembers(tx, group.ID, userIDs); err != nil {
|
||||
logrus.Errorf("[Service layer: MembershipService] syncUserGroupMembers failed, group_id=%d, err=%+v", group.ID, err)
|
||||
return status.GenErrWithCustomMsg(err, "sync user group members failed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MembershipService) DeleteUserGroup(req *dto.DeleteUserGroupRequest) error {
|
||||
if req == nil || strings.TrimSpace(req.ExternalID) == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
group, err := s.repo.GetUserGroupByExternalID(req.ExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusMembershipMetaNotFound
|
||||
}
|
||||
|
||||
if err := tx.Where("type = ? AND membership_id = ?", model.MembershipType_UserGroup, group.ID).
|
||||
Delete(&model.MembershipRelation{}).Error; err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
if err := s.repo.DeleteUserGroup(group).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MembershipService) buildUserGroupResponses(groups []model.UserGroupMeta, includeMembers bool) ([]*dto.UserGroupResponse, error) {
|
||||
if len(groups) == 0 {
|
||||
return []*dto.UserGroupResponse{}, nil
|
||||
}
|
||||
|
||||
groupIDs := make([]int64, 0, len(groups))
|
||||
creatorIDs := make([]int64, 0, len(groups))
|
||||
for _, g := range groups {
|
||||
groupIDs = append(groupIDs, g.ID)
|
||||
creatorIDs = append(creatorIDs, g.CreatorID)
|
||||
}
|
||||
creatorIDs = gslice.Uniq(creatorIDs)
|
||||
|
||||
relations, err := s.repo.ListMembershipByTypeAndIDs(model.MembershipType_UserGroup, groupIDs).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
|
||||
creatorMap, err := s.useRepo.MGetUserByID(creatorIDs).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
|
||||
groupMemberUserIDs := make(map[int64][]int64, len(groupIDs))
|
||||
groupMemberSeen := make(map[int64]map[int64]struct{}, len(groupIDs))
|
||||
for _, relation := range relations {
|
||||
if _, ok := groupMemberSeen[relation.MembershipID]; !ok {
|
||||
groupMemberSeen[relation.MembershipID] = map[int64]struct{}{}
|
||||
}
|
||||
if _, exists := groupMemberSeen[relation.MembershipID][relation.UserID]; exists {
|
||||
continue
|
||||
}
|
||||
groupMemberSeen[relation.MembershipID][relation.UserID] = struct{}{}
|
||||
|
||||
groupMemberUserIDs[relation.MembershipID] = append(groupMemberUserIDs[relation.MembershipID], relation.UserID)
|
||||
}
|
||||
|
||||
responses := make([]*dto.UserGroupResponse, 0, len(groups))
|
||||
for _, g := range groups {
|
||||
resp := &dto.UserGroupResponse{
|
||||
ExternalID: g.ExternalID,
|
||||
Name: g.Name,
|
||||
Description: g.Description,
|
||||
MemberCount: len(groupMemberUserIDs[g.ID]),
|
||||
}
|
||||
if creator := creatorMap[g.CreatorID]; creator != nil {
|
||||
resp.CreatorUserExternalID = creator.ExternalID
|
||||
}
|
||||
responses = append(responses, resp)
|
||||
}
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) resolveUserExternalIDsToIDs(externalIDs []string, tx *gorm.DB) ([]int64, error) {
|
||||
if len(externalIDs) == 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
|
||||
deduped := make([]string, 0, len(externalIDs))
|
||||
seen := map[string]struct{}{}
|
||||
for _, ext := range externalIDs {
|
||||
ext = strings.TrimSpace(ext)
|
||||
if ext == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
if _, ok := seen[ext]; ok {
|
||||
continue
|
||||
}
|
||||
seen[ext] = struct{}{}
|
||||
deduped = append(deduped, ext)
|
||||
}
|
||||
|
||||
users, err := s.useRepo.ListByExternal(deduped).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
if len(users) != len(deduped) {
|
||||
return nil, status.StatusUserNotFound
|
||||
}
|
||||
|
||||
userIDs := make([]int64, 0, len(users))
|
||||
for _, user := range users {
|
||||
userIDs = append(userIDs, user.ID)
|
||||
}
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
func (s *MembershipService) syncUserGroupMembers(tx *gorm.DB, groupID int64, targetUserIDs []int64) error {
|
||||
existingRelations, err := s.repo.ListMembership(&model.MembershipRelation{
|
||||
Type: model.MembershipType_UserGroup,
|
||||
MembershipID: groupID,
|
||||
}).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusReadDBError
|
||||
}
|
||||
|
||||
existingUserIDs := map[int64]struct{}{}
|
||||
for _, relation := range existingRelations {
|
||||
existingUserIDs[relation.UserID] = struct{}{}
|
||||
}
|
||||
targetUserIDSet := map[int64]struct{}{}
|
||||
for _, userID := range targetUserIDs {
|
||||
targetUserIDSet[userID] = struct{}{}
|
||||
}
|
||||
|
||||
toCreate := make([]*model.MembershipRelation, 0)
|
||||
for userID := range targetUserIDSet {
|
||||
if _, ok := existingUserIDs[userID]; ok {
|
||||
continue
|
||||
}
|
||||
toCreate = append(toCreate, &model.MembershipRelation{
|
||||
Type: model.MembershipType_UserGroup,
|
||||
UserID: userID,
|
||||
MembershipID: groupID,
|
||||
})
|
||||
}
|
||||
|
||||
toDeleteUserIDs := make([]int64, 0)
|
||||
for userID := range existingUserIDs {
|
||||
if _, ok := targetUserIDSet[userID]; ok {
|
||||
continue
|
||||
}
|
||||
toDeleteUserIDs = append(toDeleteUserIDs, userID)
|
||||
}
|
||||
|
||||
if len(toCreate) > 0 {
|
||||
if err := s.repo.BatchCreateMembership(toCreate).WithTx(tx).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
}
|
||||
if len(toDeleteUserIDs) > 0 {
|
||||
if err := tx.Where("type = ? AND membership_id = ? AND user_id IN ?", model.MembershipType_UserGroup, groupID, toDeleteUserIDs).
|
||||
Delete(&model.MembershipRelation{}).Error; err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package mq_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/config"
|
||||
svc_iface "github.com/XingfenD/yoresee_doc/internal/service/interface"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/mq"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MQService struct {
|
||||
backend mq.Backend
|
||||
queues map[mq.Backend]mq.MessageQueue
|
||||
}
|
||||
|
||||
var MQSvc = &MQService{
|
||||
backend: mq.BackendRedis,
|
||||
queues: map[mq.Backend]mq.MessageQueue{},
|
||||
}
|
||||
|
||||
func (srvc *MQService) IsInitialized() bool {
|
||||
return srvc != nil && len(srvc.queues) > 0
|
||||
}
|
||||
|
||||
func (srvc *MQService) Init(cfg *config.MQConfig) error {
|
||||
if srvc == nil {
|
||||
return status.StatusMQNotInitialized
|
||||
}
|
||||
|
||||
var options *mq.Options
|
||||
if cfg != nil {
|
||||
options = &mq.Options{RabbitMQURL: cfg.RabbitMQ.URL}
|
||||
}
|
||||
queues, err := mq.NewMessageQueues(options)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MQService] Init failed, err=%+v", err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "init message queue failed")
|
||||
}
|
||||
srvc.queues = queues
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srvc *MQService) Close() error {
|
||||
if srvc == nil {
|
||||
return status.StatusMQNotInitialized
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for backend, q := range srvc.queues {
|
||||
if q == nil {
|
||||
continue
|
||||
}
|
||||
if err := q.Close(); err != nil {
|
||||
logrus.Errorf("[Service layer: MQService] Close backend failed, backend=%s, err=%+v", backend, err)
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
srvc.queues = map[mq.Backend]mq.MessageQueue{}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (srvc *MQService) Publish(ctx context.Context, backend mq.Backend, msg mq.PublishMessage) error {
|
||||
if !srvc.IsInitialized() {
|
||||
return status.StatusMQNotInitialized
|
||||
}
|
||||
|
||||
msg.Topic = strings.TrimSpace(msg.Topic)
|
||||
if msg.Topic == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "publish message failed")
|
||||
}
|
||||
|
||||
q, err := srvc.getQueue(backend)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MQService] Publish getQueue failed, backend=%s, err=%+v", backend, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "publish message failed")
|
||||
}
|
||||
|
||||
if err := q.Publish(ctx, msg); err != nil {
|
||||
logrus.Errorf("[Service layer: MQService] Publish failed, backend=%s, topic=%s, err=%+v", backend, msg.Topic, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "publish message failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srvc *MQService) Consume(ctx context.Context, backend mq.Backend, opts mq.ConsumeOptions, handler func(context.Context, mq.Message) error) error {
|
||||
if !srvc.IsInitialized() {
|
||||
return status.StatusMQNotInitialized
|
||||
}
|
||||
opts = normalizeConsumeOptions(opts)
|
||||
if opts.Topic == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "subscribe message failed")
|
||||
}
|
||||
if err := validateConsumeOptions(opts); err != nil {
|
||||
logrus.Errorf("[Service layer: MQService] Consume options invalid, backend=%s, topic=%s, err=%+v", backend, opts.Topic, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "subscribe message failed")
|
||||
}
|
||||
|
||||
q, err := srvc.getQueue(backend)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MQService] Consume getQueue failed, backend=%s, topic=%s, err=%+v", backend, opts.Topic, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "subscribe message failed")
|
||||
}
|
||||
|
||||
if err := q.Consume(ctx, opts, handler); err != nil {
|
||||
logrus.Errorf("[Service layer: MQService] Consume failed, backend=%s, topic=%s, err=%+v", backend, opts.Topic, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "subscribe message failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PublishByProducer[T any](ctx context.Context, producer svc_iface.TopicProducer, taskData T) error {
|
||||
if !MQSvc.IsInitialized() {
|
||||
return status.StatusMQNotInitialized
|
||||
}
|
||||
data, err := sonic.Marshal(taskData)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MQService] marshal failed, producer=%s, err=%+v", producer.Topic(), err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "marshal message failed")
|
||||
}
|
||||
return MQSvc.Publish(ctx, MQSvc.backend, mq.PublishMessage{
|
||||
Topic: producer.Topic(),
|
||||
Body: data,
|
||||
})
|
||||
}
|
||||
|
||||
func PublishByProducerTo[T any](ctx context.Context, backend mq.Backend, producer svc_iface.TopicProducer, taskData T) error {
|
||||
if !MQSvc.IsInitialized() {
|
||||
return status.StatusMQNotInitialized
|
||||
}
|
||||
data, err := sonic.Marshal(taskData)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: MQService] marshal failed, backend=%s, producer=%s, err=%+v", backend, producer.Topic(), err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "marshal message failed")
|
||||
}
|
||||
return MQSvc.Publish(ctx, backend, mq.PublishMessage{
|
||||
Topic: producer.Topic(),
|
||||
Body: data,
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeConsumeOptions(opts mq.ConsumeOptions) mq.ConsumeOptions {
|
||||
n := opts
|
||||
n.Topic = strings.TrimSpace(n.Topic)
|
||||
n.Group = strings.TrimSpace(n.Group)
|
||||
n.Consumer = strings.TrimSpace(n.Consumer)
|
||||
if n.Mode == "" {
|
||||
n.Mode = mq.ConsumeModeFanout
|
||||
}
|
||||
if n.OnError == "" {
|
||||
n.OnError = mq.ErrorActionRequeue
|
||||
}
|
||||
|
||||
if n.Mode == mq.ConsumeModeGroup {
|
||||
n.Group = utils.NormalizeToken(n.Group, "default")
|
||||
}
|
||||
|
||||
consumer := n.Consumer
|
||||
if consumer == "" {
|
||||
hostName, _ := os.Hostname()
|
||||
consumer = fmt.Sprintf(
|
||||
"%s-%s-%d",
|
||||
utils.NormalizeToken(n.Topic, "default"),
|
||||
utils.NormalizeToken(hostName, "default"),
|
||||
os.Getpid(),
|
||||
)
|
||||
}
|
||||
n.Consumer = utils.NormalizeToken(consumer, "default")
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func validateConsumeOptions(opts mq.ConsumeOptions) error {
|
||||
switch opts.Mode {
|
||||
case mq.ConsumeModeFanout, mq.ConsumeModeGroup:
|
||||
default:
|
||||
return fmt.Errorf("invalid consume mode: %s", opts.Mode)
|
||||
}
|
||||
|
||||
switch opts.OnError {
|
||||
case mq.ErrorActionDrop, mq.ErrorActionRequeue:
|
||||
default:
|
||||
return fmt.Errorf("invalid error action: %s", opts.OnError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srvc *MQService) getQueue(backend mq.Backend) (mq.MessageQueue, error) {
|
||||
if srvc == nil || len(srvc.queues) == 0 {
|
||||
return nil, fmt.Errorf("message queue not initialized")
|
||||
}
|
||||
q, ok := srvc.queues[backend]
|
||||
if !ok || q == nil {
|
||||
return nil, fmt.Errorf("message queue backend not initialized: %s", backend)
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package notification_service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/notification_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type NotificationService struct {
|
||||
notificationRepo *notification_repo.NotificationRepository
|
||||
userRepo *user_repo.UserRepository
|
||||
}
|
||||
|
||||
func NewNotificationService(repos *repository.Repositories) *NotificationService {
|
||||
return &NotificationService{
|
||||
notificationRepo: repos.Notification,
|
||||
userRepo: repos.User,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NotificationService) CreateNotifications(req *dto.CreateNotificationRequest) error {
|
||||
if req == nil || len(req.ReceiverExternalIDs) == 0 {
|
||||
return status.StatusParamError
|
||||
}
|
||||
if strings.TrimSpace(req.Type) == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
|
||||
users, err := s.userRepo.ListByExternal(req.ReceiverExternalIDs).Exec()
|
||||
if err != nil {
|
||||
return status.StatusReadDBError
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
userIDMap := make(map[string]int64, len(users))
|
||||
for _, user := range users {
|
||||
userIDMap[user.ExternalID] = user.ID
|
||||
}
|
||||
|
||||
items := make([]model.Notification, 0, len(req.ReceiverExternalIDs))
|
||||
for _, externalID := range req.ReceiverExternalIDs {
|
||||
id, ok := userIDMap[externalID]
|
||||
if !ok {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
items = append(items, model.Notification{
|
||||
ExternalID: utils.GenerateExternalID(utils.ExternalIDContextNotification),
|
||||
ReceiverID: id,
|
||||
Type: req.Type,
|
||||
Status: "unread",
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Payload: req.PayloadJSON,
|
||||
})
|
||||
}
|
||||
|
||||
if err := s.notificationRepo.CreateBatch(items).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NotificationService) ListNotifications(req *dto.ListNotificationsRequest) ([]model.Notification, int64, error) {
|
||||
if req == nil || strings.TrimSpace(req.UserExternalID) == "" {
|
||||
return nil, 0, status.StatusParamError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusUserNotFound
|
||||
}
|
||||
list, total, err := s.notificationRepo.List(userID).
|
||||
WithStatus(req.Status).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: NotificationService] list notifications failed, user_external_id=%s, err=%+v", req.UserExternalID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusReadDBError, "list notifications failed")
|
||||
}
|
||||
|
||||
for i := range list {
|
||||
if strings.TrimSpace(list[i].ExternalID) == "" {
|
||||
externalID := utils.GenerateExternalID(utils.ExternalIDContextNotification)
|
||||
list[i].ExternalID = externalID
|
||||
_ = s.notificationRepo.UpdateExternalID(list[i].ID, externalID).Exec()
|
||||
}
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
func (s *NotificationService) MarkRead(req *dto.MarkNotificationsReadRequest) error {
|
||||
if req == nil || strings.TrimSpace(req.UserExternalID) == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
if err := s.notificationRepo.MarkRead(userID, req.ExternalIDs).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NotificationService) MarkAllRead(userExternalID string) error {
|
||||
if strings.TrimSpace(userExternalID) == "" {
|
||||
return status.StatusParamError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(userExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
if err := s.notificationRepo.MarkAllRead(userID).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var NotificationSvc *NotificationService
|
||||
@@ -0,0 +1 @@
|
||||
package service
|
||||
@@ -0,0 +1,169 @@
|
||||
package setting_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/constant"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/config_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/storage"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SettingOption struct {
|
||||
Label string
|
||||
LabelKey string
|
||||
Value string
|
||||
}
|
||||
|
||||
type SettingUI struct {
|
||||
Component string
|
||||
Options []SettingOption
|
||||
Placeholder string
|
||||
PlaceholderKey string
|
||||
}
|
||||
|
||||
type SettingItem struct {
|
||||
Key string
|
||||
Label string
|
||||
LabelKey string
|
||||
Description string
|
||||
DescriptionKey string
|
||||
Type string
|
||||
UI SettingUI
|
||||
Value string
|
||||
DefaultValue string
|
||||
Required bool
|
||||
Readonly bool
|
||||
}
|
||||
|
||||
type SettingGroup struct {
|
||||
Key string
|
||||
Title string
|
||||
TitleKey string
|
||||
Items []SettingItem
|
||||
}
|
||||
|
||||
type SettingUpdate struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type SettingService struct{}
|
||||
|
||||
var SettingSvc *SettingService
|
||||
|
||||
func InitSettingService() {
|
||||
SettingSvc = &SettingService{}
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSettings(ctx context.Context, scene string) ([]SettingGroup, error) {
|
||||
if scene == "" || scene == "system" || scene == "manage" {
|
||||
return s.buildSystemSecuritySettings(ctx), nil
|
||||
}
|
||||
return []SettingGroup{}, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateSettings(ctx context.Context, updates []SettingUpdate) error {
|
||||
for _, update := range updates {
|
||||
key := strings.TrimSpace(update.Key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case systemRegisterModeKey():
|
||||
val := strings.TrimSpace(update.Value)
|
||||
if val != constant.RegisterMode_Open && val != constant.RegisterMode_Invite {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid register mode")
|
||||
}
|
||||
if err := config_service.ConfigSvc.Set(ctx, key, val); err != nil {
|
||||
logrus.Errorf("[Service layer: SettingService] set register mode failed, key=%s, value=%s, err=%+v", key, val, err)
|
||||
return status.GenErrWithCustomMsg(err, "update register mode failed")
|
||||
}
|
||||
storage.Consul.ClearCacheKey(key)
|
||||
case systemRegisterLimitKey():
|
||||
val := strings.ToLower(strings.TrimSpace(update.Value))
|
||||
boolVal := val == "true" || val == "1" || val == "on"
|
||||
writeVal := constant.RegisterLimit_Off
|
||||
if boolVal {
|
||||
writeVal = constant.RegisterLimit_On
|
||||
}
|
||||
if err := config_service.ConfigSvc.Set(ctx, key, writeVal); err != nil {
|
||||
logrus.Errorf("[Service layer: SettingService] set register limit failed, key=%s, value=%s, err=%+v", key, writeVal, err)
|
||||
return status.GenErrWithCustomMsg(err, "update register limit failed")
|
||||
}
|
||||
storage.Consul.ClearCacheKey(key)
|
||||
default:
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "unknown setting key")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SettingService) buildSystemSecuritySettings(ctx context.Context) []SettingGroup {
|
||||
registerMode := config_service.ConfigSvc.GetSystemRegisterMode(ctx)
|
||||
registerLimit := config_service.ConfigSvc.GetSystemRegisterLimit(ctx)
|
||||
limitValue := "false"
|
||||
if registerLimit {
|
||||
limitValue = "true"
|
||||
}
|
||||
|
||||
return []SettingGroup{
|
||||
{
|
||||
Key: "security",
|
||||
TitleKey: "system.security.registration",
|
||||
Items: []SettingItem{
|
||||
{
|
||||
Key: systemRegisterModeKey(),
|
||||
LabelKey: "system.security.registrationMode",
|
||||
Type: "string",
|
||||
Value: registerMode,
|
||||
DefaultValue: constant.RegisterMode_Invite,
|
||||
Required: true,
|
||||
UI: SettingUI{
|
||||
Component: "radio",
|
||||
Options: []SettingOption{
|
||||
{
|
||||
LabelKey: "system.security.freeRegister",
|
||||
Value: constant.RegisterMode_Open,
|
||||
},
|
||||
{
|
||||
LabelKey: "system.security.inviteOnly",
|
||||
Value: constant.RegisterMode_Invite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: systemRegisterLimitKey(),
|
||||
LabelKey: "system.security.registerLimit",
|
||||
DescriptionKey: "system.security.registerLimitDesc",
|
||||
Type: "bool",
|
||||
Value: limitValue,
|
||||
DefaultValue: "false",
|
||||
UI: SettingUI{
|
||||
Component: "switch",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func systemRegisterModeKey() string {
|
||||
return utils.GenConfigKey(
|
||||
constant.ConfigKey_First_System,
|
||||
constant.ConfigKey_Second_Security,
|
||||
constant.ConfigKey_Third_RegisterMode,
|
||||
)
|
||||
}
|
||||
|
||||
func systemRegisterLimitKey() string {
|
||||
return utils.GenConfigKey(
|
||||
constant.ConfigKey_First_System,
|
||||
constant.ConfigKey_Second_Security,
|
||||
constant.ConfigKey_Third_RegisterLimit,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package user_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
userRepo *user_repo.UserRepository
|
||||
}
|
||||
|
||||
func NewUserService(repos *repository.Repositories) *UserService {
|
||||
return &UserService{
|
||||
userRepo: repos.User,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserService) GetByExternalID(externalID string) (*dto.UserResponse, error) {
|
||||
userModel, err := s.userRepo.GetByExternalID(externalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: UserService] GetByExternalID failed, external_id=%s, err=%+v", externalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusUserNotFound, "user not found")
|
||||
}
|
||||
return dto.NewUserResponseFromModel(userModel), nil
|
||||
}
|
||||
|
||||
func (s *UserService) GetByID(id int64) (*dto.UserResponse, error) {
|
||||
userModel, err := s.userRepo.GetByID(id).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: UserService] GetByID failed, id=%d, err=%+v", id, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusUserNotFound, "user not found")
|
||||
}
|
||||
return dto.NewUserResponseFromModel(userModel), nil
|
||||
}
|
||||
|
||||
func (s *UserService) GetIDByExternalID(externalID string) (int64, error) {
|
||||
id, err := s.userRepo.GetIDByExternalID(externalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: UserService] GetIDByExternalID failed, external_id=%s, err=%+v", externalID, err)
|
||||
return 0, status.GenErrWithCustomMsg(status.StatusUserNotFound, "user not found")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
var UserSvc *UserService
|
||||
Reference in New Issue
Block a user