205 lines
6.0 KiB
Go
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
|
|
} |