239 lines
6.6 KiB
Go
239 lines
6.6 KiB
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
|
|
} |