Basdat/internal/services/match_service.go
2025-12-20 00:01:08 +07:00

205 lines
6.0 KiB
Go

// internal/services/match_service.go
package services
import (
"encoding/json"
"lost-and-found/internal/models"
"lost-and-found/internal/repositories"
"lost-and-found/internal/utils"
"gorm.io/gorm"
)
type MatchService struct {
db *gorm.DB // Tambahkan ini
matchRepo *repositories.MatchResultRepository
itemRepo *repositories.ItemRepository
lostItemRepo *repositories.LostItemRepository
notificationRepo *repositories.NotificationRepository
}
func NewMatchService(db *gorm.DB) *MatchService {
return &MatchService{
db: db, // Tambahkan ini
matchRepo: repositories.NewMatchResultRepository(db),
itemRepo: repositories.NewItemRepository(db),
lostItemRepo: repositories.NewLostItemRepository(db),
notificationRepo: repositories.NewNotificationRepository(db),
}
}
// MatchedField represents a matched field between items
type MatchedField struct {
Field string `json:"field"`
LostValue string `json:"lost_value"`
FoundValue string `json:"found_value"`
Score float64 `json:"score"`
}
// FindSimilarItems finds similar items for a lost item report
func (s *MatchService) FindSimilarItems(lostItemID uint) ([]models.MatchResultResponse, error) {
lostItem, err := s.lostItemRepo.FindByID(lostItemID)
if err != nil {
return nil, err
}
// Search for items in same category
items, err := s.itemRepo.SearchForMatching(lostItem.CategoryID, lostItem.Name, lostItem.Color)
if err != nil {
return nil, err
}
var results []models.MatchResultResponse
for _, item := range items {
// Calculate similarity
score, matchedFields := s.calculateSimilarity(lostItem, &item)
// Only include if score is reasonable (>= 30%)
if score >= 30.0 {
// Check if match already exists
exists, _ := s.matchRepo.CheckExistingMatch(lostItemID, item.ID)
if !exists {
// Create match result
matchedFieldsJSON, _ := json.Marshal(matchedFields)
match := &models.MatchResult{
LostItemID: lostItemID,
ItemID: item.ID,
SimilarityScore: score,
MatchedFields: string(matchedFieldsJSON),
IsNotified: false,
}
s.matchRepo.Create(match)
// Reload with relations
match, _ = s.matchRepo.FindByID(match.ID)
results = append(results, match.ToResponse())
}
}
}
return results, nil
}
// GetMatchesForLostItem gets all matches for a lost item
func (s *MatchService) GetMatchesForLostItem(lostItemID uint) ([]models.MatchResultResponse, error) {
matches, err := s.matchRepo.FindByLostItem(lostItemID)
if err != nil {
return nil, err
}
var responses []models.MatchResultResponse
for _, match := range matches {
responses = append(responses, match.ToResponse())
}
return responses, nil
}
// GetMatchesForItem gets all matches for an item
func (s *MatchService) GetMatchesForItem(itemID uint) ([]models.MatchResultResponse, error) {
matches, err := s.matchRepo.FindByItem(itemID)
if err != nil {
return nil, err
}
var responses []models.MatchResultResponse
for _, match := range matches {
responses = append(responses, match.ToResponse())
}
return responses, nil
}
// AutoMatchNewItem automatically matches a new item with lost items
func (s *MatchService) AutoMatchNewItem(itemID uint) error {
item, err := s.itemRepo.FindByID(itemID)
if err != nil {
return err
}
// Find active lost items in same category
lostItems, err := s.lostItemRepo.FindActiveForMatching(item.CategoryID)
if err != nil {
return err
}
for _, lostItem := range lostItems {
// Calculate similarity
score, matchedFields := s.calculateSimilarity(&lostItem, item)
// Create match if score is high enough (>= 50% for auto-match)
if score >= 50.0 {
// Check if match already exists
exists, _ := s.matchRepo.CheckExistingMatch(lostItem.ID, itemID)
if !exists {
matchedFieldsJSON, _ := json.Marshal(matchedFields)
match := &models.MatchResult{
LostItemID: lostItem.ID,
ItemID: itemID,
SimilarityScore: score,
MatchedFields: string(matchedFieldsJSON),
IsNotified: false,
}
s.matchRepo.Create(match)
// Send notification to lost item owner - PERBAIKAN DI SINI
models.CreateMatchNotification(s.db, lostItem.UserID, item.Name, match.ID)
s.matchRepo.MarkAsNotified(match.ID)
}
}
}
return nil
}
// calculateSimilarity calculates similarity between lost item and found item
func (s *MatchService) calculateSimilarity(lostItem *models.LostItem, item *models.Item) (float64, []MatchedField) {
var matchedFields []MatchedField
// 1. Hard Filter: Kategori HARUS sama.
if lostItem.CategoryID != item.CategoryID {
return 0.0, matchedFields
}
// --- A. Hitung Kecocokan Nama (50%) ---
nameSim := utils.CalculateStringSimilarity(lostItem.Name, item.Name)
nameScore := nameSim * 100.0
if nameScore > 40.0 {
matchedFields = append(matchedFields, MatchedField{
Field: "name",
LostValue: lostItem.Name,
FoundValue: item.Name,
Score: nameScore,
})
}
// --- B. Hitung Kecocokan Secret Details / Deskripsi (50%) ---
// Target: Utamakan Secret Details penemu, fallback ke Description
targetText := item.SecretDetails
foundValueType := "Secret Details"
if targetText == "" {
targetText = item.Description
foundValueType = "Description"
}
// Bandingkan Deskripsi User vs Target Penemu
descSim := utils.CalculateStringSimilarity(lostItem.Description, targetText)
descScore := descSim * 100.0
if descScore > 40.0 {
matchedFields = append(matchedFields, MatchedField{
Field: "secret_details",
LostValue: "User Description",
FoundValue: foundValueType,
Score: descScore,
})
}
// --- C. Hitung Skor Akhir ---
// Rumus: (Skor Nama * 0.5) + (Skor Deskripsi * 0.5)
finalScore := (nameScore * 0.5) + (descScore * 0.5)
return finalScore, matchedFields
}