added AI auto-generated keywords to customer profile

This commit is contained in:
Mazyar
2026-01-04 13:44:45 +03:30
parent 3381e04e05
commit 8dcda0e031
9 changed files with 435 additions and 48 deletions
+47
View File
@@ -5,6 +5,7 @@ import (
"time" "time"
"tm/pkg/authorization" "tm/pkg/authorization"
"tm/pkg/config" "tm/pkg/config"
"tm/pkg/glm"
"tm/pkg/hcaptcha" "tm/pkg/hcaptcha"
"tm/pkg/logger" "tm/pkg/logger"
"tm/pkg/mongo" "tm/pkg/mongo"
@@ -268,3 +269,49 @@ func InitHCaptchaService(conf config.HCaptchaConfig, log logger.Logger) hcaptcha
return hcaptchaVerifier return hcaptchaVerifier
} }
// InitGLMService initializes the GLM service
func InitGLMService(conf GLMConfig, log logger.Logger) *glm.SDK {
glmSDK, err := glm.New(
&glm.Config{
BaseURL: conf.BaseURL,
APIKey: conf.APIKey,
Timeout: conf.Timeout,
RetryAttempts: conf.RetryAttempts,
RetryDelay: conf.RetryDelay,
EnableLogging: conf.EnableLogging,
UserAgent: conf.UserAgent,
DefaultModel: conf.DefaultModel,
DefaultMaxTokens: conf.DefaultMaxTokens,
DefaultTopP: conf.DefaultTopP,
DefaultTemperature: conf.DefaultTemperature,
DefaultFrequencyPenalty: conf.DefaultFrequencyPenalty,
DefaultPresencePenalty: conf.DefaultPresencePenalty,
EnableStreaming: conf.EnableStreaming,
EnableThinking: conf.EnableThinking,
TranslationOptions: glm.TranslationOption{
Model: conf.TranslateOptions.Model,
MaxTokens: conf.TranslateOptions.MaxTokens,
Temperature: conf.TranslateOptions.Temperature,
FrequencyPenalty: conf.TranslateOptions.FrequencyPenalty,
PresencePenalty: conf.TranslateOptions.PresencePenalty,
TopP: conf.TranslateOptions.TopP,
EnableThinking: conf.TranslateOptions.EnableThinking,
},
}, log)
if err != nil {
log.Error("Failed to initialize GLM SDK", map[string]interface{}{
"error": err.Error(),
})
log.Warn("GLM service will be disabled, keyword generation features will not be available", map[string]interface{}{})
return nil
}
log.Info("GLM SDK initialized successfully", map[string]interface{}{
"base_url": conf.BaseURL,
"timeout": conf.Timeout,
})
return glmSDK
}
+30
View File
@@ -16,11 +16,41 @@ type Config struct {
Notification config.NotificationConfig Notification config.NotificationConfig
Ollama config.OllamaConfig Ollama config.OllamaConfig
HCaptcha config.HCaptchaConfig HCaptcha config.HCaptchaConfig
GLM GLMConfig
Scraper ScraperConfig Scraper ScraperConfig
UserAuth AuthConfig UserAuth AuthConfig
CustomerAuth AuthConfig CustomerAuth AuthConfig
} }
type GLMConfig struct {
BaseURL string `env:"GLM_BASE_URL" envDefault:"https://api.z.ai"`
APIKey string `env:"GLM_API_KEY" envDefault:""`
Timeout time.Duration `env:"GLM_TIMEOUT" envDefault:"300s"`
RetryAttempts int `env:"GLM_RETRY_ATTEMPTS" envDefault:"3"`
RetryDelay time.Duration `env:"GLM_RETRY_DELAY" envDefault:"2s"`
EnableLogging bool `env:"GLM_ENABLE_LOGGING" envDefault:"true"`
UserAgent string `env:"GLM_USER_AGENT" envDefault:"Opplens-GLMSDK/1.0"`
DefaultModel string `env:"GLM_DEFAULT_MODEL" envDefault:"glm-4.5"`
DefaultMaxTokens int `env:"GLM_DEFAULT_MAX_TOKENS" envDefault:"4096"`
DefaultTopP float64 `env:"GLM_DEFAULT_TOP_P" envDefault:"1.0"`
DefaultTemperature float64 `env:"GLM_DEFAULT_TEMPERATURE" envDefault:"0.7"`
DefaultFrequencyPenalty float64 `env:"GLM_DEFAULT_FREQUENCY_PENALTY" envDefault:"0"`
DefaultPresencePenalty float64 `env:"GLM_DEFAULT_PRESENCE_PENALTY" envDefault:"0"`
EnableStreaming bool `env:"GLM_ENABLE_STREAMING" envDefault:"false"`
EnableThinking bool `env:"GLM_ENABLE_THINKING" envDefault:"false"`
TranslateOptions TranslateOptions
}
type TranslateOptions struct {
Model string `env:"GLM_TRANSLATION_MODEL" envDefault:"glm-4.5"`
MaxTokens int `env:"GLM_TRANSLATION_MAX_TOKENS" envDefault:"2048"`
Temperature float64 `env:"GLM_TRANSLATION_TEMPERATURE" envDefault:"0.3"`
FrequencyPenalty float64 `env:"GLM_TRANSLATION_FREQUENCY_PENALTY" envDefault:"0.1"`
PresencePenalty float64 `env:"GLM_TRANSLATION_PRESENCE_PENALTY" envDefault:"0.1"`
TopP float64 `env:"GLM_TRANSLATION_TOP_P" envDefault:"1.0"`
EnableThinking bool `env:"GLM_TRANSLATION_ENABLE_THINKING" envDefault:"false"`
}
type ScraperConfig struct { type ScraperConfig struct {
BaseURL string `env:"SCRAPER_BASE_URL"` BaseURL string `env:"SCRAPER_BASE_URL"`
Timeout time.Duration `env:"SCRAPER_TIMEOUT"` Timeout time.Duration `env:"SCRAPER_TIMEOUT"`
+5 -2
View File
@@ -149,6 +149,9 @@ func main() {
// Initialize hCaptcha verifier // Initialize hCaptcha verifier
hcaptchaVerifier := bootstrap.InitHCaptchaService(conf.HCaptcha, logger) hcaptchaVerifier := bootstrap.InitHCaptchaService(conf.HCaptcha, logger)
// Initialize GLM service
glmSDK := bootstrap.InitGLMService(conf.GLM, logger)
// Initialize authorization service // Initialize authorization service
userAuthService := bootstrap.InitAuthorizationService(conf.UserAuth.JWT, redisClient, logger) userAuthService := bootstrap.InitAuthorizationService(conf.UserAuth.JWT, redisClient, logger)
customerAuthService := bootstrap.InitAuthorizationService(conf.CustomerAuth.JWT, redisClient, logger) customerAuthService := bootstrap.InitAuthorizationService(conf.CustomerAuth.JWT, redisClient, logger)
@@ -176,8 +179,8 @@ func main() {
userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK) userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK)
categoryService := company_category.NewService(categoryRepository, logger) categoryService := company_category.NewService(categoryRepository, logger)
companyService := company.NewService(companyRepository, categoryService, logger) companyService := company.NewService(companyRepository, categoryService, logger)
customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK, customerValidator) customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK, customerValidator, glmSDK)
tenderService := tender.NewService(tenderRepository, companyService, logger) tenderService := tender.NewService(tenderRepository, companyService, customerService, logger)
feedbackService := feedback.NewService(feedbackRepo, tenderService, companyService, logger) feedbackService := feedback.NewService(feedbackRepo, tenderService, companyService, logger)
tenderApprovalService := tender_approval.NewService(tenderApprovalRepository, tenderService, logger) tenderApprovalService := tender_approval.NewService(tenderApprovalRepository, tenderService, logger)
inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger, xssPolicy) inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger, xssPolicy)
+14 -2
View File
@@ -32,6 +32,16 @@ const (
CustomerRoleAnalyst CustomerRole = "analyst" CustomerRoleAnalyst CustomerRole = "analyst"
) )
// KeywordsStatus represents the status of keyword processing
type KeywordsStatus string
const (
KeywordsStatusPending KeywordsStatus = "pending"
KeywordsStatusInProgress KeywordsStatus = "in_progress"
KeywordsStatusReady KeywordsStatus = "ready"
KeywordsStatusFailed KeywordsStatus = "failed"
)
// Customer represents a customer in the tender management system // Customer represents a customer in the tender management system
// A customer can have an associated company for login purposes // A customer can have an associated company for login purposes
type Customer struct { type Customer struct {
@@ -44,8 +54,10 @@ type Customer struct {
Type CustomerType `bson:"type"` Type CustomerType `bson:"type"`
Role CustomerRole `bson:"role"` Role CustomerRole `bson:"role"`
Phone *string `bson:"phone"` Phone *string `bson:"phone"`
Companies []string `bson:"companies"` Companies []string `bson:"companies"`
DeviceToken []string `bson:"device_token"` Keywords []string `bson:"keywords,omitempty"`
KeywordsStatus KeywordsStatus `bson:"keywords_status,omitempty"`
DeviceToken []string `bson:"device_token"`
LastLoginAt *int64 `bson:"last_login_at"` LastLoginAt *int64 `bson:"last_login_at"`
UpdatedAt int64 `bson:"updated_at"` UpdatedAt int64 `bson:"updated_at"`
CreatedAt int64 `bson:"created_at"` CreatedAt int64 `bson:"created_at"`
+27 -14
View File
@@ -28,6 +28,7 @@ type UpdateCustomerForm struct {
Username string `json:"username" valid:"required,username,length(3|30)" example:"user"` Username string `json:"username" valid:"required,username,length(3|30)" example:"user"`
Email string `json:"email" valid:"required,email" example:"user@opplens.com"` Email string `json:"email" valid:"required,email" example:"user@opplens.com"`
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"` Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
Keywords []string `json:"keywords,omitempty" valid:"optional"`
} }
// CustomerStatusForm represents the form for suspending a customer // CustomerStatusForm represents the form for suspending a customer
@@ -63,25 +64,37 @@ type CustomerResponse struct {
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
LastLoginAt *int64 `json:"last_login_at"` LastLoginAt *int64 `json:"last_login_at"`
Companies []*CompanySummary `json:"companies"` Companies []*CompanySummary `json:"companies"`
DeviceToken []string `json:"-"` Keywords []string `json:"keywords"`
KeywordsStatus string `json:"keywords_status"`
DeviceToken []string `json:"-"`
} }
// ToResponse converts Customer to CustomerResponse // ToResponse converts Customer to CustomerResponse
func (c *Customer) ToResponse(companies []*CompanySummary) *CustomerResponse { func (c *Customer) ToResponse(companies []*CompanySummary) *CustomerResponse {
keywords := c.Keywords
if keywords == nil {
keywords = []string{}
}
keywordsStatus := string(c.KeywordsStatus)
if keywordsStatus == "" {
keywordsStatus = string(KeywordsStatusReady)
}
return &CustomerResponse{ return &CustomerResponse{
ID: c.ID.Hex(), ID: c.ID.Hex(),
FullName: c.FullName, FullName: c.FullName,
Username: c.Username, Username: c.Username,
Email: c.Email, Email: c.Email,
Type: string(c.Type), Type: string(c.Type),
Status: string(c.Status), Status: string(c.Status),
Role: string(c.Role), Role: string(c.Role),
Phone: c.Phone, Phone: c.Phone,
UpdatedAt: c.UpdatedAt, UpdatedAt: c.UpdatedAt,
CreatedAt: c.CreatedAt, CreatedAt: c.CreatedAt,
LastLoginAt: c.LastLoginAt, LastLoginAt: c.LastLoginAt,
Companies: companies, Companies: companies,
Keywords: keywords,
KeywordsStatus: keywordsStatus,
} }
} }
+212 -4
View File
@@ -11,6 +11,7 @@ import (
"time" "time"
"tm/internal/company" "tm/internal/company"
"tm/pkg/glm"
"tm/pkg/logger" "tm/pkg/logger"
"tm/pkg/notification" "tm/pkg/notification"
"tm/pkg/redis" "tm/pkg/redis"
@@ -61,10 +62,11 @@ type customerService struct {
redisClient redis.Client redisClient redis.Client
notification notification.SDK notification notification.SDK
validator ValidationService validator ValidationService
glmSDK *glm.SDK
} }
// New creates a new customer service // New creates a new customer service
func NewService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK, validator ValidationService) Service { func NewService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK, validator ValidationService, glmSDK *glm.SDK) Service {
return &customerService{ return &customerService{
repository: repository, repository: repository,
logger: logger, logger: logger,
@@ -73,6 +75,7 @@ func NewService(repository Repository, logger logger.Logger, authService authori
redisClient: redisClient, redisClient: redisClient,
notification: notificationSDK, notification: notificationSDK,
validator: validator, validator: validator,
glmSDK: glmSDK,
} }
} }
@@ -127,6 +130,21 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
Phone: form.Phone, Phone: form.Phone,
} }
// Generate keywords using GLM AI
customer.KeywordsStatus = KeywordsStatusInProgress
keywords, err := s.generateKeywords(ctx, customer)
if err != nil {
s.logger.Warn("Failed to generate keywords, continuing without keywords", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
keywords = []string{}
customer.KeywordsStatus = KeywordsStatusFailed
} else {
customer.Keywords = keywords
customer.KeywordsStatus = KeywordsStatusReady
}
// Save to database // Save to database
err = s.repository.Register(ctx, customer) err = s.repository.Register(ctx, customer)
if err != nil { if err != nil {
@@ -335,7 +353,24 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
// Handle company assignments // Handle company assignments
companies := make([]*CompanySummary, 0) companies := make([]*CompanySummary, 0)
companiesChanged := false
if len(form.Companies) > 0 { if len(form.Companies) > 0 {
// Check if companies changed
if len(form.Companies) != len(customer.Companies) {
companiesChanged = true
} else {
companyMap := make(map[string]bool)
for _, id := range customer.Companies {
companyMap[id] = true
}
for _, id := range form.Companies {
if !companyMap[id] {
companiesChanged = true
break
}
}
}
customer.Companies = form.Companies
companies, err = s.loadCompaniesForCustomer(ctx, customer) companies, err = s.loadCompaniesForCustomer(ctx, customer)
if err != nil { if err != nil {
s.logger.Error("Failed to load companies for customer", map[string]interface{}{ s.logger.Error("Failed to load companies for customer", map[string]interface{}{
@@ -345,17 +380,101 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
} }
} }
if form.FullName != nil { infoChanged := false
if form.FullName != nil && (customer.FullName == nil || *form.FullName != *customer.FullName) {
customer.FullName = form.FullName customer.FullName = form.FullName
infoChanged = true
} }
if form.Phone != nil { if form.Phone != nil && (customer.Phone == nil || *form.Phone != *customer.Phone) {
customer.Phone = form.Phone customer.Phone = form.Phone
infoChanged = true
} }
// Update role if provided // Update role if provided
if form.Role != "" { if form.Role != "" && customer.Role != CustomerRole(form.Role) {
customer.Role = CustomerRole(form.Role) customer.Role = CustomerRole(form.Role)
infoChanged = true
}
// Handle keywords
if form.Keywords != nil {
// Keywords explicitly provided by customer, set status to in_progress
// Customer's manual keywords are saved, but we'll enhance them with AI
customer.Keywords = form.Keywords
customer.KeywordsStatus = KeywordsStatusInProgress
// Save customer with in_progress status first, then process asynchronously
customerID := customer.ID.Hex()
go func() {
ctx := context.Background()
// Get fresh customer data for processing
cust, err := s.repository.GetByID(ctx, customerID)
if err != nil {
s.logger.Error("Failed to get customer for keyword processing", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return
}
// If GLM SDK is not available, keep manual keywords and set status to ready
if s.glmSDK == nil {
s.logger.Info("GLM SDK not available, keeping manual keywords", map[string]interface{}{
"customer_id": customerID,
})
cust.KeywordsStatus = KeywordsStatusReady
} else {
// Regenerate keywords based on updated customer info
keywords, err := s.generateKeywords(ctx, cust)
if err != nil {
s.logger.Warn("Failed to process keywords after manual update", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
// Keep manual keywords but set status to failed
cust.KeywordsStatus = KeywordsStatusFailed
} else if len(keywords) > 0 {
// Update with AI-generated keywords
cust.Keywords = keywords
cust.KeywordsStatus = KeywordsStatusReady
} else {
// Empty keywords from GLM, keep manual keywords and set status to ready
s.logger.Warn("GLM returned empty keywords, keeping manual keywords", map[string]interface{}{
"customer_id": customerID,
})
cust.KeywordsStatus = KeywordsStatusReady
}
}
// Update customer with new status
err = s.repository.Update(ctx, cust)
if err != nil {
s.logger.Error("Failed to update customer keywords status", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
} else {
s.logger.Info("Keywords processed successfully", map[string]interface{}{
"customer_id": customerID,
"status": cust.KeywordsStatus,
})
}
}()
} else if infoChanged || companiesChanged {
// Customer info changed, regenerate keywords
customer.KeywordsStatus = KeywordsStatusInProgress
keywords, err := s.generateKeywords(ctx, customer)
if err != nil {
s.logger.Warn("Failed to regenerate keywords, keeping existing keywords", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
customer.KeywordsStatus = KeywordsStatusFailed
} else {
customer.Keywords = keywords
customer.KeywordsStatus = KeywordsStatusReady
}
} }
// Update in database // Update in database
@@ -1244,3 +1363,92 @@ func (s *customerService) AssignRole(ctx context.Context, customerID string, for
Success: true, Success: true,
}, nil }, nil
} }
// generateKeywords generates keywords for a customer using GLM AI based on their information
func (s *customerService) generateKeywords(ctx context.Context, customer *Customer) ([]string, error) {
// If GLM SDK is not available, return empty keywords
if s.glmSDK == nil {
s.logger.Warn("GLM SDK not available, skipping keyword generation", map[string]interface{}{
"customer_id": customer.ID,
})
return []string{}, nil
}
// Build customer information string for AI
var customerInfo strings.Builder
if customer.FullName != nil {
customerInfo.WriteString(fmt.Sprintf("Name: %s\n", *customer.FullName))
}
customerInfo.WriteString(fmt.Sprintf("Type: %s\n", customer.Type))
customerInfo.WriteString(fmt.Sprintf("Role: %s\n", customer.Role))
if customer.Phone != nil {
customerInfo.WriteString(fmt.Sprintf("Phone: %s\n", *customer.Phone))
}
// Get company information if available
if len(customer.Companies) > 0 {
customerInfo.WriteString("Companies: ")
for i, companyID := range customer.Companies {
comp, err := s.companyService.GetByID(ctx, companyID)
if err == nil {
if i > 0 {
customerInfo.WriteString(", ")
}
customerInfo.WriteString(comp.Name)
if comp.Industry != "" {
customerInfo.WriteString(fmt.Sprintf(" (Industry: %s)", comp.Industry))
}
}
}
customerInfo.WriteString("\n")
}
systemPrompt := `You are a keyword extraction assistant for a tender management system.
Analyze the customer information and generate 5-10 relevant keywords that would help match them with appropriate tenders.
Keywords should be:
- Relevant to tender/procurement categories
- Based on customer type, role, and company information
- Professional and specific
- Suitable for matching with tender descriptions
Return ONLY a comma-separated list of keywords, no explanations or additional text.`
userMessage := fmt.Sprintf("Generate keywords for this customer:\n\n%s", customerInfo.String())
s.logger.Info("Generating keywords using GLM AI", map[string]interface{}{
"customer_id": customer.ID,
})
response, err := s.glmSDK.QuickChatWithSystem(ctx, systemPrompt, userMessage)
if err != nil {
s.logger.Error("Failed to generate keywords using GLM AI", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
return []string{}, fmt.Errorf("failed to generate keywords: %w", err)
}
// Parse comma-separated keywords
keywordsStr := strings.TrimSpace(response)
if keywordsStr == "" {
return []string{}, nil
}
// Split by comma and clean up
keywordList := strings.Split(keywordsStr, ",")
keywords := make([]string, 0, len(keywordList))
for _, kw := range keywordList {
kw = strings.TrimSpace(kw)
if kw != "" {
keywords = append(keywords, kw)
}
}
s.logger.Info("Keywords generated successfully", map[string]interface{}{
"customer_id": customer.ID,
"keywords": keywords,
"count": len(keywords),
})
return keywords, nil
}
+1
View File
@@ -21,6 +21,7 @@ type SearchForm struct {
CountryCodes []string `query:"country_codes" valid:"optional"` CountryCodes []string `query:"country_codes" valid:"optional"`
RegionCodes []string `query:"region_codes" valid:"optional"` RegionCodes []string `query:"region_codes" valid:"optional"`
CompanyID *string `query:"company_id" valid:"optional"` CompanyID *string `query:"company_id" valid:"optional"`
CustomerID *string `query:"customer_id" valid:"optional"`
Status []string `query:"status" valid:"optional"` Status []string `query:"status" valid:"optional"`
Classifications []string `query:"classifications" valid:"optional"` Classifications []string `query:"classifications" valid:"optional"`
MinEstimatedValue *float64 `query:"min_estimated_value" valid:"optional"` MinEstimatedValue *float64 `query:"min_estimated_value" valid:"optional"`
+13 -2
View File
@@ -216,13 +216,19 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
form.Status = []string{string(TenderStatusActive)} form.Status = []string{string(TenderStatusActive)}
// Get company ID from customer context for automatic matching // Get company ID and customer ID from customer context for automatic matching
var companyID *string var companyID *string
if customerCompanyID, ok := c.Get("company_id").(string); ok { if customerCompanyID, ok := c.Get("company_id").(string); ok {
companyID = &customerCompanyID companyID = &customerCompanyID
} }
form.CompanyID = companyID form.CompanyID = companyID
var customerID *string
if customerIDStr, ok := c.Get("customer_id").(string); ok {
customerID = &customerIDStr
}
form.CustomerID = customerID
result, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c)) result, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
if err != nil { if err != nil {
return response.InternalServerError(c, "Failed to retrieve tenders") return response.InternalServerError(c, "Failed to retrieve tenders")
@@ -277,9 +283,14 @@ func (h *TenderHandler) RecommendTenders(c echo.Context) error {
if customerCompanyID, ok := c.Get("company_id").(string); ok { if customerCompanyID, ok := c.Get("company_id").(string); ok {
companyID = &customerCompanyID companyID = &customerCompanyID
} }
form.CompanyID = companyID form.CompanyID = companyID
var customerID *string
if customerIDStr, ok := c.Get("customer_id").(string); ok {
customerID = &customerIDStr
}
form.CustomerID = customerID
result, err := h.service.Recommend(c.Request().Context(), form, response.NewPagination(c)) result, err := h.service.Recommend(c.Request().Context(), form, response.NewPagination(c))
if err != nil { if err != nil {
return response.InternalServerError(c, "Failed to retrieve tenders") return response.InternalServerError(c, "Failed to retrieve tenders")
+86 -24
View File
@@ -3,8 +3,10 @@ package tender
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"tm/internal/company" "tm/internal/company"
"tm/internal/customer"
"tm/pkg/logger" "tm/pkg/logger"
"tm/pkg/response" "tm/pkg/response"
) )
@@ -26,17 +28,19 @@ type Service interface {
// tenderService implements TenderService interface // tenderService implements TenderService interface
type tenderService struct { type tenderService struct {
repository TenderRepository repository TenderRepository
companyService company.Service companyService company.Service
logger logger.Logger customerService customer.Service
logger logger.Logger
} }
// NewService creates a new tender service // NewService creates a new tender service
func NewService(repository TenderRepository, companyService company.Service, logger logger.Logger) Service { func NewService(repository TenderRepository, companyService company.Service, customerService customer.Service, logger logger.Logger) Service {
return &tenderService{ return &tenderService{
repository: repository, repository: repository,
companyService: companyService, companyService: companyService,
logger: logger, customerService: customerService,
logger: logger,
} }
} }
@@ -156,8 +160,8 @@ func (s *tenderService) GetDocumentSummaries(ctx context.Context, id string) ([]
} }
s.logger.Info("Retrieved document summaries for tender", map[string]interface{}{ s.logger.Info("Retrieved document summaries for tender", map[string]interface{}{
"tender_id": id, "tender_id": id,
"summaries_count": len(tender.DocumentSummaries), "summaries_count": len(tender.DocumentSummaries),
}) })
return tender.DocumentSummaries, nil return tender.DocumentSummaries, nil
@@ -166,13 +170,41 @@ func (s *tenderService) GetDocumentSummaries(ctx context.Context, id string) ([]
// ListTenders retrieves tenders with pagination and filtering // ListTenders retrieves tenders with pagination and filtering
func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) { func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
s.logger.Info("Listing tenders", map[string]interface{}{ s.logger.Info("Listing tenders", map[string]interface{}{
"limit": pagination.Limit, "limit": pagination.Limit,
"offset": pagination.Offset, "offset": pagination.Offset,
"company_id": form.CompanyID, "company_id": form.CompanyID,
"from": form.DeadlineFrom, "customer_id": form.CustomerID,
"to": form.DeadlineTo, "from": form.DeadlineFrom,
"to": form.DeadlineTo,
}) })
// If customer ID is provided, get customer keywords and add to search
if form.CustomerID != nil && *form.CustomerID != "" {
customerResp, err := s.customerService.GetByID(ctx, *form.CustomerID)
if err == nil && customerResp != nil {
// Only use keywords if status is ready
if customerResp.KeywordsStatus == "ready" && len(customerResp.Keywords) > 0 {
// Combine customer keywords with existing search query
keywordsQuery := strings.Join(customerResp.Keywords, " ")
if form.Search != nil && *form.Search != "" {
combinedSearch := *form.Search + " " + keywordsQuery
form.Search = &combinedSearch
} else {
form.Search = &keywordsQuery
}
s.logger.Info("Using customer keywords for search", map[string]interface{}{
"customer_id": *form.CustomerID,
"keywords": customerResp.Keywords,
})
} else {
s.logger.Info("Customer keywords not ready, skipping keyword-based search", map[string]interface{}{
"customer_id": *form.CustomerID,
"keywords_status": customerResp.KeywordsStatus,
})
}
}
}
tenders, total, err := s.repository.Search(ctx, form, pagination) tenders, total, err := s.repository.Search(ctx, form, pagination)
if err != nil { if err != nil {
s.logger.Error("Failed to list tenders", map[string]interface{}{ s.logger.Error("Failed to list tenders", map[string]interface{}{
@@ -240,20 +272,50 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
// Recommend retrieves tenders with pagination and filtering // Recommend retrieves tenders with pagination and filtering
func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) { func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
s.logger.Info("Recommend tenders", map[string]interface{}{ s.logger.Info("Recommend tenders", map[string]interface{}{
"company_id": form.CompanyID, "company_id": form.CompanyID,
"customer_id": form.CustomerID,
}) })
company, err := s.companyService.GetByID(ctx, *form.CompanyID) form.Status = []string{string(TenderStatusActive)}
if err != nil {
s.logger.Error("Failed to get company", map[string]interface{}{ // Priority: Use customer keywords if available, otherwise use company CPV codes
"company_id": form.CompanyID, if form.CustomerID != nil && *form.CustomerID != "" {
"error": err.Error(), customerResp, err := s.customerService.GetByID(ctx, *form.CustomerID)
}) if err == nil && customerResp != nil {
return nil, fmt.Errorf("failed to get company: %w", err) // Only use keywords if status is ready
if customerResp.KeywordsStatus == "ready" && len(customerResp.Keywords) > 0 {
// Use customer keywords for recommendation
keywordsQuery := strings.Join(customerResp.Keywords, " ")
form.Search = &keywordsQuery
s.logger.Info("Using customer keywords for recommendation", map[string]interface{}{
"customer_id": *form.CustomerID,
"keywords": customerResp.Keywords,
})
} else {
s.logger.Info("Customer keywords not ready, falling back to company CPV codes", map[string]interface{}{
"customer_id": *form.CustomerID,
"keywords_status": customerResp.KeywordsStatus,
})
}
}
} }
form.Status = []string{string(TenderStatusActive)} // Fallback to company CPV codes if no customer keywords
form.Classifications = company.Tags.CPVCodes if form.Search == nil || *form.Search == "" {
if form.CompanyID != nil && *form.CompanyID != "" {
company, err := s.companyService.GetByID(ctx, *form.CompanyID)
if err != nil {
s.logger.Error("Failed to get company", map[string]interface{}{
"company_id": form.CompanyID,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get company: %w", err)
}
if company.Tags != nil && len(company.Tags.CPVCodes) > 0 {
form.Classifications = company.Tags.CPVCodes
}
}
}
tenders, total, err := s.repository.Search(ctx, form, pagination) tenders, total, err := s.repository.Search(ctx, form, pagination)
if err != nil { if err != nil {