mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
This commit includes the following changes: - Refactored all data repositories in `internal/data/sql/` to use a consistent `sql` package and to align with the new `domain` models. - Fixed the GraphQL structure by moving the server creation logic from `internal/app` to `cmd/api`, which resolved an import cycle. - Corrected numerous incorrect import paths for packages like `graph`, `linguistics`, `syncjob`, and the legacy `models` package. - Resolved several package and function redeclaration errors. - Removed legacy migration code.
218 lines
6.4 KiB
Go
218 lines
6.4 KiB
Go
package linguistics
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"tercul/internal/domain"
|
|
"time"
|
|
|
|
"tercul/internal/platform/log"
|
|
)
|
|
|
|
// WorkAnalysisService defines the interface for work-specific analysis operations
|
|
type WorkAnalysisService interface {
|
|
// AnalyzeWork performs linguistic analysis on a work
|
|
AnalyzeWork(ctx context.Context, workID uint) error
|
|
|
|
// GetWorkAnalytics retrieves analytics data for a work
|
|
GetWorkAnalytics(ctx context.Context, workID uint) (*WorkAnalytics, error)
|
|
}
|
|
|
|
// WorkAnalytics contains analytics data for a work
|
|
type WorkAnalytics struct {
|
|
WorkID uint
|
|
ViewCount int64
|
|
LikeCount int64
|
|
CommentCount int64
|
|
BookmarkCount int64
|
|
TranslationCount int64
|
|
ReadabilityScore float64
|
|
SentimentScore float64
|
|
TopKeywords []string
|
|
PopularTranslations []TranslationAnalytics
|
|
}
|
|
|
|
// TranslationAnalytics contains analytics data for a translation
|
|
type TranslationAnalytics struct {
|
|
TranslationID uint
|
|
Language string
|
|
ViewCount int64
|
|
LikeCount int64
|
|
}
|
|
|
|
// workAnalysisService implements the WorkAnalysisService interface
|
|
type workAnalysisService struct {
|
|
textAnalyzer TextAnalyzer
|
|
analysisCache AnalysisCache
|
|
analysisRepo AnalysisRepository
|
|
concurrency int
|
|
cacheEnabled bool
|
|
}
|
|
|
|
// NewWorkAnalysisService creates a new WorkAnalysisService
|
|
func NewWorkAnalysisService(
|
|
textAnalyzer TextAnalyzer,
|
|
analysisCache AnalysisCache,
|
|
analysisRepo AnalysisRepository,
|
|
concurrency int,
|
|
cacheEnabled bool,
|
|
) WorkAnalysisService {
|
|
return &workAnalysisService{
|
|
textAnalyzer: textAnalyzer,
|
|
analysisCache: analysisCache,
|
|
analysisRepo: analysisRepo,
|
|
concurrency: concurrency,
|
|
cacheEnabled: cacheEnabled,
|
|
}
|
|
}
|
|
|
|
// AnalyzeWork performs linguistic analysis on a work and stores the results
|
|
func (s *workAnalysisService) AnalyzeWork(ctx context.Context, workID uint) error {
|
|
if workID == 0 {
|
|
return fmt.Errorf("invalid work ID")
|
|
}
|
|
|
|
// Check cache first if enabled
|
|
if s.cacheEnabled && s.analysisCache.IsEnabled() {
|
|
cacheKey := fmt.Sprintf("work_analysis:%d", workID)
|
|
|
|
if result, err := s.analysisCache.Get(ctx, cacheKey); err == nil {
|
|
log.LogInfo("Cache hit for work analysis",
|
|
log.F("workID", workID))
|
|
|
|
// Store directly to database
|
|
return s.analysisRepo.StoreAnalysisResults(ctx, workID, result)
|
|
}
|
|
}
|
|
|
|
// Get work content from database
|
|
content, err := s.analysisRepo.GetWorkContent(ctx, workID, "")
|
|
if err != nil {
|
|
log.LogError("Failed to get work content for analysis",
|
|
log.F("workID", workID),
|
|
log.F("error", err))
|
|
return fmt.Errorf("failed to get work content: %w", err)
|
|
}
|
|
|
|
// Skip analysis if content is empty
|
|
if content == "" {
|
|
log.LogWarn("Skipping analysis for work with empty content",
|
|
log.F("workID", workID))
|
|
return nil
|
|
}
|
|
|
|
// Get work to determine language (via repository to avoid leaking GORM)
|
|
work, err := s.analysisRepo.GetWorkByID(ctx, workID)
|
|
if err != nil {
|
|
log.LogError("Failed to fetch work for analysis",
|
|
log.F("workID", workID),
|
|
log.F("error", err))
|
|
return fmt.Errorf("failed to fetch work: %w", err)
|
|
}
|
|
|
|
// Analyze the text
|
|
start := time.Now()
|
|
log.LogInfo("Analyzing work",
|
|
log.F("workID", workID),
|
|
log.F("language", work.Language),
|
|
log.F("contentLength", len(content)))
|
|
|
|
var result *AnalysisResult
|
|
|
|
// Use concurrent processing for large texts
|
|
if len(content) > 10000 && s.concurrency > 1 {
|
|
result, err = s.textAnalyzer.AnalyzeTextConcurrently(ctx, content, work.Language, s.concurrency)
|
|
} else {
|
|
result, err = s.textAnalyzer.AnalyzeText(ctx, content, work.Language)
|
|
}
|
|
|
|
if err != nil {
|
|
log.LogError("Failed to analyze work text",
|
|
log.F("workID", workID),
|
|
log.F("error", err))
|
|
return fmt.Errorf("failed to analyze work text: %w", err)
|
|
}
|
|
|
|
// Store results in database
|
|
if err := s.analysisRepo.StoreAnalysisResults(ctx, workID, result); err != nil {
|
|
log.LogError("Failed to store analysis results",
|
|
log.F("workID", workID),
|
|
log.F("error", err))
|
|
return fmt.Errorf("failed to store analysis results: %w", err)
|
|
}
|
|
|
|
// Cache the result if caching is enabled
|
|
if s.cacheEnabled && s.analysisCache.IsEnabled() {
|
|
cacheKey := fmt.Sprintf("work_analysis:%d", workID)
|
|
if err := s.analysisCache.Set(ctx, cacheKey, result); err != nil {
|
|
log.LogWarn("Failed to cache work analysis result",
|
|
log.F("workID", workID),
|
|
log.F("error", err))
|
|
}
|
|
}
|
|
|
|
log.LogInfo("Successfully analyzed work",
|
|
log.F("workID", workID),
|
|
log.F("wordCount", result.WordCount),
|
|
log.F("readabilityScore", result.ReadabilityScore),
|
|
log.F("sentiment", result.Sentiment),
|
|
log.F("durationMs", time.Since(start).Milliseconds()))
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetWorkAnalytics retrieves analytics data for a work
|
|
func (s *workAnalysisService) GetWorkAnalytics(ctx context.Context, workID uint) (*WorkAnalytics, error) {
|
|
if workID == 0 {
|
|
return nil, fmt.Errorf("invalid work ID")
|
|
}
|
|
|
|
// Get the work to ensure it exists
|
|
work, err := s.analysisRepo.GetWorkByID(ctx, workID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("work not found: %w", err)
|
|
}
|
|
|
|
// Get analysis results from database
|
|
_, readabilityScore, languageAnalysis, _ := s.analysisRepo.GetAnalysisData(ctx, workID)
|
|
|
|
// Extract keywords from JSONB
|
|
var keywords []string
|
|
if languageAnalysis.Analysis != nil {
|
|
if keywordsData, ok := languageAnalysis.Analysis["keywords"].([]interface{}); ok {
|
|
for _, kw := range keywordsData {
|
|
if keywordMap, ok := kw.(map[string]interface{}); ok {
|
|
if text, ok := keywordMap["text"].(string); ok {
|
|
keywords = append(keywords, text)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// For now, return placeholder analytics with actual analysis data
|
|
return &WorkAnalytics{
|
|
WorkID: work.ID,
|
|
ViewCount: 0, // TODO: Implement view counting
|
|
LikeCount: 0, // TODO: Implement like counting
|
|
CommentCount: 0, // TODO: Implement comment counting
|
|
BookmarkCount: 0, // TODO: Implement bookmark counting
|
|
TranslationCount: 0, // TODO: Implement translation counting
|
|
ReadabilityScore: readabilityScore.Score,
|
|
SentimentScore: extractSentimentFromAnalysis(languageAnalysis.Analysis),
|
|
TopKeywords: keywords,
|
|
PopularTranslations: []TranslationAnalytics{}, // TODO: Implement translation analytics
|
|
}, nil
|
|
}
|
|
|
|
// extractSentimentFromAnalysis extracts sentiment from the Analysis JSONB field
|
|
func extractSentimentFromAnalysis(analysis domain.JSONB) float64 {
|
|
if analysis == nil {
|
|
return 0.0
|
|
}
|
|
if sentiment, ok := analysis["sentiment"].(float64); ok {
|
|
return sentiment
|
|
}
|
|
return 0.0
|
|
}
|