company feedback performance update

This commit is contained in:
Mazyar
2026-05-25 11:27:55 +03:30
parent 3c86d35663
commit 51defd9438
4 changed files with 163 additions and 5 deletions
+25
View File
@@ -16,6 +16,7 @@ type Service interface {
Update(ctx context.Context, id string, form *CompanyForm) (*CompanyResponse, error) Update(ctx context.Context, id string, form *CompanyForm) (*CompanyResponse, error)
GetByID(ctx context.Context, id string) (*CompanyResponse, error) GetByID(ctx context.Context, id string) (*CompanyResponse, error)
GetByIDs(ctx context.Context, ids []string) ([]*CompanyResponse, error) GetByIDs(ctx context.Context, ids []string) ([]*CompanyResponse, error)
GetNamesByIDs(ctx context.Context, ids []string) (map[string]*CompanyResponse, error)
Delete(ctx context.Context, id string) error Delete(ctx context.Context, id string) error
// Company listing and search // Company listing and search
@@ -389,6 +390,30 @@ func (s *companyService) GetByIDs(ctx context.Context, ids []string) ([]*Company
return companyResponses, nil return companyResponses, nil
} }
// GetNamesByIDs returns company id and name keyed by company ID without loading categories.
func (s *companyService) GetNamesByIDs(ctx context.Context, ids []string) (map[string]*CompanyResponse, error) {
if len(ids) == 0 {
return map[string]*CompanyResponse{}, nil
}
companies, err := s.repository.GetByIDs(ctx, ids)
if err != nil {
s.logger.Error("Failed to get companies by IDs for names", map[string]interface{}{
"count": len(ids),
"error": err.Error(),
})
return nil, errors.New("failed to get companies by IDs")
}
out := make(map[string]*CompanyResponse, len(companies))
for _, c := range companies {
id := c.ID.Hex()
out[id] = &CompanyResponse{ID: id, Name: c.Name}
}
return out, nil
}
// UpdateCompanyStatus updates company status // UpdateCompanyStatus updates company status
func (s *companyService) UpdateStatus(ctx context.Context, id string, form *StatusForm) error { func (s *companyService) UpdateStatus(ctx context.Context, id string, form *StatusForm) error {
// Get company to check if exists // Get company to check if exists
+82 -2
View File
@@ -137,10 +137,19 @@ func (s *feedbackService) Search(ctx context.Context, criteria SearchForm, pagin
meta := pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset) meta := pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset)
tenderByID, companyByID := s.loadListDetails(ctx, page.Items)
result := make([]*FeedbackResponse, len(page.Items)) result := make([]*FeedbackResponse, len(page.Items))
for i, feedback := range page.Items { for i, feedback := range page.Items {
tender, company := s.getDetails(ctx, feedback.TenderID, feedbackCompanyID(feedback)) var tenderResp *tenderResponse
result[i] = feedback.ToResponse(tender, company) if t, ok := tenderByID[feedback.TenderID]; ok {
tenderResp = t
}
var companyResp *companyResponse
if cid := feedbackCompanyID(feedback); cid != "" {
companyResp = companyByID[cid]
}
result[i] = feedback.ToResponse(tenderResp, companyResp)
} }
return &FeedbackListResponse{ return &FeedbackListResponse{
@@ -175,6 +184,77 @@ func feedbackCompanyID(f Feedback) string {
return *f.CompanyID return *f.CompanyID
} }
func uniqueNonEmptyStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
out := make([]string, 0, len(values))
for _, v := range values {
if v == "" {
continue
}
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
return out
}
func collectFeedbackListIDs(items []Feedback) (tenderIDs, companyIDs []string) {
tenderIDs = make([]string, 0, len(items))
companyIDs = make([]string, 0, len(items))
for _, item := range items {
tenderIDs = append(tenderIDs, item.TenderID)
companyIDs = append(companyIDs, feedbackCompanyID(item))
}
return uniqueNonEmptyStrings(tenderIDs), uniqueNonEmptyStrings(companyIDs)
}
// loadListDetails batch-loads tender and company data for feedback list responses.
func (s *feedbackService) loadListDetails(ctx context.Context, items []Feedback) (
map[string]*tenderResponse,
map[string]*companyResponse,
) {
tenderByID := make(map[string]*tenderResponse)
companyByID := make(map[string]*companyResponse)
if len(items) == 0 {
return tenderByID, companyByID
}
tenderIDs, companyIDs := collectFeedbackListIDs(items)
if len(tenderIDs) > 0 {
tenders, err := s.tenderService.GetByIDs(ctx, tenderIDs)
if err != nil {
s.logger.Error("Failed to batch load tenders for feedback list", map[string]interface{}{
"count": len(tenderIDs),
"error": err.Error(),
})
} else {
for id, t := range tenders {
tenderByID[id] = tenderToFeedbackResponse(t)
}
}
}
if len(companyIDs) > 0 {
companies, err := s.companyService.GetNamesByIDs(ctx, companyIDs)
if err != nil {
s.logger.Error("Failed to batch load companies for feedback list", map[string]interface{}{
"count": len(companyIDs),
"error": err.Error(),
})
} else {
for id, c := range companies {
companyByID[id] = companyToFeedbackResponse(c)
}
}
}
return tenderByID, companyByID
}
func tenderToFeedbackResponse(t *tender.TenderResponse) *tenderResponse { func tenderToFeedbackResponse(t *tender.TenderResponse) *tenderResponse {
if t == nil { if t == nil {
return nil return nil
+28
View File
@@ -18,6 +18,7 @@ type TenderRepository interface {
// Tender CRUD operations // Tender CRUD operations
Create(ctx context.Context, tender *Tender) error Create(ctx context.Context, tender *Tender) error
GetByID(ctx context.Context, id string) (*Tender, error) GetByID(ctx context.Context, id string) (*Tender, error)
GetByIDs(ctx context.Context, ids []string) ([]Tender, error)
GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error)
GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error) GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error)
GetByContractFolderID(ctx context.Context, contractFolderID string) (*Tender, error) GetByContractFolderID(ctx context.Context, contractFolderID string) (*Tender, error)
@@ -214,6 +215,33 @@ func (r *tenderRepository) GetByID(ctx context.Context, id string) (*Tender, err
return tender, nil return tender, nil
} }
// GetByIDs retrieves tenders by IDs in a single query.
func (r *tenderRepository) GetByIDs(ctx context.Context, ids []string) ([]Tender, error) {
if len(ids) == 0 {
return nil, nil
}
objectIDs := make([]bson.ObjectID, 0, len(ids))
for _, id := range ids {
objectID, err := bson.ObjectIDFromHex(id)
if err != nil {
return nil, err
}
objectIDs = append(objectIDs, objectID)
}
result, err := r.ormRepo.FindAll(ctx, bson.M{"_id": bson.M{"$in": objectIDs}}, orm.NewPaginationBuilder().Build())
if err != nil {
r.logger.Error("Failed to get tenders by IDs", map[string]interface{}{
"count": len(ids),
"error": err.Error(),
})
return nil, err
}
return result.Items, nil
}
// GetByContractNoticeID retrieves a tender by contract notice ID // GetByContractNoticeID retrieves a tender by contract notice ID
func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) { func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) {
filter := bson.M{"contract_notice_id": contractNoticeID} filter := bson.M{"contract_notice_id": contractNoticeID}
+28 -3
View File
@@ -47,6 +47,7 @@ type TenderDocumentDownload struct {
type Service interface { type Service interface {
// Tender operations // Tender operations
GetByID(ctx context.Context, id string, language string) (*TenderResponse, error) GetByID(ctx context.Context, id string, language string) (*TenderResponse, error)
GetByIDs(ctx context.Context, ids []string) (map[string]*TenderResponse, error)
Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error) Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error)
Delete(ctx context.Context, id string) error Delete(ctx context.Context, id string) error
Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error) Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
@@ -82,7 +83,7 @@ type tenderService struct {
} }
const ( const (
aiSummaryEnrichmentTimeout = 2 * time.Second aiSummaryEnrichmentTimeout = 2 * time.Second
aiTranslationEnrichmentTimeout = 2 * time.Second aiTranslationEnrichmentTimeout = 2 * time.Second
) )
@@ -128,6 +129,30 @@ func (s *tenderService) GetByID(ctx context.Context, id string, language string)
return s.buildDetailResponse(ctx, tender, language), nil return s.buildDetailResponse(ctx, tender, language), nil
} }
// GetByIDs loads multiple tenders for list endpoints without MinIO or AI enrichment.
func (s *tenderService) GetByIDs(ctx context.Context, ids []string) (map[string]*TenderResponse, error) {
if len(ids) == 0 {
return map[string]*TenderResponse{}, nil
}
tenders, err := s.repository.GetByIDs(ctx, ids)
if err != nil {
s.logger.Error("Failed to retrieve tenders by IDs", map[string]interface{}{
"count": len(ids),
"error": err.Error(),
})
return nil, fmt.Errorf("failed to retrieve tenders: %w", err)
}
out := make(map[string]*TenderResponse, len(tenders))
for i := range tenders {
resp := tenders[i].ToResponseWithLanguage("")
out[resp.ID] = resp
}
return out, nil
}
// buildDetailResponse maps a tender to API form and enriches translation (MinIO) and summary. // buildDetailResponse maps a tender to API form and enriches translation (MinIO) and summary.
func (s *tenderService) buildDetailResponse(ctx context.Context, tender *Tender, language string) *TenderResponse { func (s *tenderService) buildDetailResponse(ctx context.Context, tender *Tender, language string) *TenderResponse {
resp := tender.ToResponseWithLanguage("") resp := tender.ToResponseWithLanguage("")
@@ -1271,8 +1296,8 @@ func (s *tenderService) ListTendersWithScrapedDocuments(ctx context.Context, pag
} }
s.logger.Info("Listing tenders with scraped documents from MinIO", map[string]interface{}{ s.logger.Info("Listing tenders with scraped documents from MinIO", map[string]interface{}{
"limit": pagination.Limit, "limit": pagination.Limit,
"offset": pagination.Offset, "offset": pagination.Offset,
"sync_mongo_metadata": syncMongoMetadata, "sync_mongo_metadata": syncMongoMetadata,
}) })