migrate from github

This commit is contained in:
2026-07-15 22:42:31 +08:00
commit 3ad559f05e
262 changed files with 21637 additions and 0 deletions
@@ -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
}
+127
View File
@@ -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
}
+142
View File
@@ -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"`
}
+63
View File
@@ -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
}
+17
View File
@@ -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
}
+206
View File
@@ -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