2025-11-23 22:49:46 +07:00

240 lines
6.6 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
totalScore := 0.0
maxScore := 0.0
// Category match (20 points)
maxScore += 20
if lostItem.CategoryID == item.CategoryID {
totalScore += 20
matchedFields = append(matchedFields, MatchedField{
Field: "category",
LostValue: lostItem.Category.Name,
FoundValue: item.Category.Name,
Score: 20,
})
}
// Name similarity (30 points)
maxScore += 30
nameSimilarity := utils.CalculateStringSimilarity(lostItem.Name, item.Name)
nameScore := nameSimilarity * 30
totalScore += nameScore
if nameScore > 10 {
matchedFields = append(matchedFields, MatchedField{
Field: "name",
LostValue: lostItem.Name,
FoundValue: item.Name,
Score: nameScore,
})
}
// Color match (15 points)
if lostItem.Color != "" {
maxScore += 15
colorSimilarity := utils.CalculateStringSimilarity(lostItem.Color, item.Name+" "+item.Description)
colorScore := colorSimilarity * 15
totalScore += colorScore
if colorScore > 5 {
matchedFields = append(matchedFields, MatchedField{
Field: "color",
LostValue: lostItem.Color,
FoundValue: "matched in description",
Score: colorScore,
})
}
}
// Location match (20 points)
if lostItem.Location != "" {
maxScore += 20
locationSimilarity := utils.CalculateStringSimilarity(lostItem.Location, item.Location)
locationScore := locationSimilarity * 20
totalScore += locationScore
if locationScore > 10 {
matchedFields = append(matchedFields, MatchedField{
Field: "location",
LostValue: lostItem.Location,
FoundValue: item.Location,
Score: locationScore,
})
}
}
// Description keywords match (15 points)
maxScore += 15
descSimilarity := utils.CalculateStringSimilarity(lostItem.Description, item.Description)
descScore := descSimilarity * 15
totalScore += descScore
if descScore > 5 {
matchedFields = append(matchedFields, MatchedField{
Field: "description",
LostValue: "keywords matched",
FoundValue: "keywords matched",
Score: descScore,
})
}
// Calculate percentage
percentage := (totalScore / maxScore) * 100
if percentage > 100 {
percentage = 100
}
return percentage, matchedFields
}