向宁 11315fd00b feat: wire Watermill CQRS — EventBus in usecases, event handlers, router lifecycle
- Add EventPublisher interface in biz layer for domain event publishing
- Wire EventBusPublisher (Watermill EventBus adapter) into FileUsecase, FolderUsecase, ShareUsecase
- Publish events after UploadFile, DeleteFile, CreateFolder, DeleteFolder, CreateShare
- Implement CQRSHandler with logging event handlers for all 6 event types
- Register event handlers via CQRSBus.RegisterHandlers using Watermill EventProcessor
- Store subscriber and wmLogger in CQRSBus for EventProcessor wiring
- Expose SqlDB() on Data struct for Watermill SQL pub/sub
- Start Watermill router in goroutine alongside Kratos app with graceful close
- Use appContext wrapper struct to pass CQRSBus through Wire DI graph
2026-05-25 13:52:05 +08:00

162 lines
4.9 KiB
Go

package biz
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"time"
"rag/file-system/internal/data"
"rag/file-system/internal/watermark"
"github.com/go-kratos/kratos/v2/log"
)
// ShareRepo defines the interface for share link persistence operations.
type ShareRepo interface {
Create(ctx context.Context, share *data.ShareLinkPO) error
GetByToken(ctx context.Context, token string) (*data.ShareLinkPO, error)
GetByID(ctx context.Context, id string) (*data.ShareLinkPO, error)
Delete(ctx context.Context, id, createdBy string) error
IncrementDownloadCount(ctx context.Context, token string) error
ListByResource(ctx context.Context, resourceType, resourceID string) ([]data.ShareLinkPO, error)
}
// FileMetaRepoForShare is a minimal interface for share usecase to look up file metadata.
type FileMetaRepoForShare interface {
GetByID(ctx context.Context, id string) (*data.FileMetaPO, error)
}
// ShareUsecase handles share link business logic.
type ShareUsecase struct {
shareRepo ShareRepo
fileRepo FileMetaRepoForShare
s3Repo FileRepo
eventPub EventPublisher
log *log.Helper
}
// NewShareUsecase creates a new ShareUsecase.
func NewShareUsecase(shareRepo ShareRepo, fileRepo FileMetaRepoForShare, s3Repo FileRepo, eventPub EventPublisher, logger log.Logger) *ShareUsecase {
return &ShareUsecase{
shareRepo: shareRepo,
fileRepo: fileRepo,
s3Repo: s3Repo,
eventPub: eventPub,
log: log.NewHelper(logger),
}
}
// CreateShare creates a new share link with a random token.
func (uc *ShareUsecase) CreateShare(ctx context.Context, resourceType, resourceID, password string, expiresAt *time.Time, maxDownloads *int, createdBy string) (*data.ShareLinkPO, error) {
token, err := generateToken(16)
if err != nil {
return nil, fmt.Errorf("failed to generate share token: %w", err)
}
share := &data.ShareLinkPO{
ResourceType: resourceType,
ResourceID: resourceID,
Token: token,
ExpiresAt: expiresAt,
MaxDownloads: maxDownloads,
CreatedBy: createdBy,
}
if password != "" {
pwd := password
share.Password = &pwd
}
if err := uc.shareRepo.Create(ctx, share); err != nil {
return nil, fmt.Errorf("failed to create share link: %w", err)
}
// Publish domain event on success.
if err := uc.eventPub.Publish(ctx, &watermark.ShareCreatedEvent{
ShareID: share.ID,
ResourceType: share.ResourceType,
ResourceID: share.ResourceID,
Token: share.Token,
CreatedBy: share.CreatedBy,
}); err != nil {
uc.log.Errorf("failed to publish ShareCreatedEvent: %v", err)
}
return share, nil
}
// DeleteShare removes a share link by ID and creator.
func (uc *ShareUsecase) DeleteShare(ctx context.Context, id, createdBy string) error {
return uc.shareRepo.Delete(ctx, id, createdBy)
}
// GetShareInfo retrieves share details and the associated file metadata.
func (uc *ShareUsecase) GetShareInfo(ctx context.Context, token string) (*data.ShareLinkPO, *data.FileMetaPO, error) {
share, err := uc.shareRepo.GetByToken(ctx, token)
if err != nil {
return nil, nil, fmt.Errorf("failed to get share link: %w", err)
}
if share == nil {
return nil, nil, nil
}
fileMeta, err := uc.fileRepo.GetByID(ctx, share.ResourceID)
if err != nil {
return share, nil, fmt.Errorf("failed to get file metadata: %w", err)
}
return share, fileMeta, nil
}
// DownloadShare validates the share link, increments download count, and returns a presigned URL + filename.
func (uc *ShareUsecase) DownloadShare(ctx context.Context, token string) (string, string, error) {
share, err := uc.shareRepo.GetByToken(ctx, token)
if err != nil {
return "", "", fmt.Errorf("failed to get share link: %w", err)
}
if share == nil {
return "", "", fmt.Errorf("share link not found")
}
// Check expiry.
if share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now().UTC()) {
return "", "", fmt.Errorf("share link has expired")
}
// Check max downloads.
if share.MaxDownloads != nil && share.DownloadCount >= *share.MaxDownloads {
return "", "", fmt.Errorf("download limit reached")
}
// Get file metadata for bucket/key info.
fileMeta, err := uc.fileRepo.GetByID(ctx, share.ResourceID)
if err != nil {
return "", "", fmt.Errorf("failed to get file metadata: %w", err)
}
if fileMeta == nil {
return "", "", fmt.Errorf("file not found")
}
// Generate presigned URL.
url, err := uc.s3Repo.GeneratePresignedURL(ctx, fileMeta.S3Bucket, fileMeta.S3Key, 15*time.Minute)
if err != nil {
return "", "", fmt.Errorf("failed to generate download URL: %w", err)
}
// Increment download count.
if err := uc.shareRepo.IncrementDownloadCount(ctx, token); err != nil {
uc.log.Errorf("failed to increment download count for token %s: %v", token, err)
}
return url, fileMeta.Name, nil
}
// generateToken creates a cryptographically secure random hex token.
func generateToken(byteLen int) (string, error) {
b := make([]byte, byteLen)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}