package services import ( "bytes" "encoding/json" "fmt" "log" "net/http" "regexp" "strings" "time" ) // TrialExpiryChecker checks for trial expiry and sends notifications type TrialExpiryChecker struct { feishuWebhook string } // NewTrialExpiryChecker creates a new trial expiry checker func NewTrialExpiryChecker(feishuWebhook string) *TrialExpiryChecker { return &TrialExpiryChecker{ feishuWebhook: feishuWebhook, } } // Customer represents a customer record type Customer struct { ID string `json:"id"` CreatedAt string `json:"createdAt"` CustomerName string `json:"customerName"` IntendedProduct string `json:"intendedProduct"` Version string `json:"version"` Description string `json:"description"` Solution string `json:"solution"` Type string `json:"type"` Module string `json:"module"` StatusProgress string `json:"statusProgress"` Reporter string `json:"reporter"` // Now used for trial period } // TrialPeriod represents a trial period record type TrialPeriod struct { ID string `json:"id"` CustomerID string `json:"customerId"` StartTime time.Time `json:"startTime"` EndTime time.Time `json:"endTime"` CreatedAt time.Time `json:"createdAt"` } // ParseTrialPeriod parses the trial period string // Supports formats like: "2026/1/52026/1/16", "2026/1/5~2026/1/16", "2026-1-52026-1-16" func ParseTrialPeriod(trialStr string) (startDate, endDate time.Time, err error) { if trialStr == "" { return time.Time{}, time.Time{}, fmt.Errorf("empty trial period") } // Replace different separators with standard ~ trialStr = strings.ReplaceAll(trialStr, "", "~") trialStr = strings.ReplaceAll(trialStr, " ", "") // Split by ~ parts := strings.Split(trialStr, "~") if len(parts) != 2 { return time.Time{}, time.Time{}, fmt.Errorf("invalid trial period format") } // Parse dates - support multiple formats layouts := []string{ "2006/1/2", "2006-1-2", "2006/01/02", "2006-01-02", } var parseErr error for _, layout := range layouts { startDate, parseErr = time.Parse(layout, strings.TrimSpace(parts[0])) if parseErr == nil { break } } if parseErr != nil { return time.Time{}, time.Time{}, fmt.Errorf("failed to parse start date: %v", parseErr) } for _, layout := range layouts { endDate, parseErr = time.Parse(layout, strings.TrimSpace(parts[1])) if parseErr == nil { break } } if parseErr != nil { return time.Time{}, time.Time{}, fmt.Errorf("failed to parse end date: %v", parseErr) } return startDate, endDate, nil } // CheckAndNotify checks for customers whose trial expires soon and sends notifications func (t *TrialExpiryChecker) CheckAndNotify(customers []Customer) error { if t.feishuWebhook == "" { log.Println("Feishu webhook not configured, skipping trial expiry notifications") return nil } now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) for _, customer := range customers { if customer.Reporter == "" { continue } _, endDate, err := ParseTrialPeriod(customer.Reporter) if err != nil { // Skip invalid trial periods continue } // Check if trial expires within 3 days endDateOnly := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, endDate.Location()) daysUntilExpiry := int(endDateOnly.Sub(today).Hours() / 24) // Send notification for trials expiring today, tomorrow, or within 3 days if daysUntilExpiry >= 0 && daysUntilExpiry <= 3 { err := t.sendExpiryNotification(customer.CustomerName, customer.Reporter, daysUntilExpiry) if err != nil { log.Printf("Failed to send expiry notification for %s: %v", customer.CustomerName, err) } else { log.Printf("Sent trial expiry notification for customer: %s (expires in %d days)", customer.CustomerName, daysUntilExpiry) } } } return nil } // CheckTrialPeriodsAndNotify checks trial periods and sends notifications func (t *TrialExpiryChecker) CheckTrialPeriodsAndNotify(trialPeriods []TrialPeriod, customersMap map[string]string) error { if t.feishuWebhook == "" { log.Println("Feishu webhook not configured, skipping trial expiry notifications") return nil } now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) for _, period := range trialPeriods { endDateOnly := time.Date(period.EndTime.Year(), period.EndTime.Month(), period.EndTime.Day(), 0, 0, 0, 0, period.EndTime.Location()) daysUntilExpiry := int(endDateOnly.Sub(today).Hours() / 24) // Send notification for trials expiring today, tomorrow, or within 3 days if daysUntilExpiry >= 0 && daysUntilExpiry <= 3 { customerName := customersMap[period.CustomerID] if customerName == "" { customerName = period.CustomerID } trialPeriodStr := fmt.Sprintf("%s%s", period.StartTime.Format("2006/1/2 15:04"), period.EndTime.Format("2006/1/2 15:04")) err := t.sendExpiryNotification(customerName, trialPeriodStr, daysUntilExpiry) if err != nil { log.Printf("Failed to send expiry notification for %s: %v", customerName, err) } else { log.Printf("Sent trial expiry notification for customer: %s (expires in %d days)", customerName, daysUntilExpiry) } } } return nil } // sendExpiryNotification sends a rich Feishu card notification for trial expiry func (t *TrialExpiryChecker) sendExpiryNotification(customerName, trialPeriod string, daysUntilExpiry int) error { var title, urgencyLevel, urgencyColor, expiryText string // Determine urgency level and message based on days until expiry switch daysUntilExpiry { case 0: title = "🔎 试甚今日到期提醒" urgencyLevel = "玧急" urgencyColor = "red" expiryText = "今倩" case 1: title = "🟠 试甚明日到期提醒" urgencyLevel = "重芁" urgencyColor = "orange" expiryText = "明倩" case 2: title = "🟡 试甚即将到期提醒" urgencyLevel = "提醒" urgencyColor = "blue" expiryText = "2倩后" case 3: title = "🔵 试甚即将到期提醒" urgencyLevel = "提醒" urgencyColor = "blue" expiryText = "3倩后" default: title = "📅 试甚到期提醒" urgencyLevel = "提醒" urgencyColor = "blue" expiryText = fmt.Sprintf("%d倩后", daysUntilExpiry) } // Create rich card message message := map[string]interface{}{ "msg_type": "interactive", "card": map[string]interface{}{ "header": map[string]interface{}{ "title": map[string]interface{}{ "tag": "plain_text", "content": title, }, "template": urgencyColor, }, "elements": []interface{}{ map[string]interface{}{ "tag": "div", "fields": []interface{}{ map[string]interface{}{ "is_short": true, "text": map[string]interface{}{ "tag": "lark_md", "content": fmt.Sprintf("**客户名称**\n%s", customerName), }, }, map[string]interface{}{ "is_short": true, "text": map[string]interface{}{ "tag": "lark_md", "content": fmt.Sprintf("**玧急皋床**\n%s", urgencyLevel), }, }, }, }, map[string]interface{}{ "tag": "div", "fields": []interface{}{ map[string]interface{}{ "is_short": false, "text": map[string]interface{}{ "tag": "lark_md", "content": fmt.Sprintf("**试甚时闎**\n%s", trialPeriod), }, }, }, }, map[string]interface{}{ "tag": "div", "text": map[string]interface{}{ "tag": "lark_md", "content": fmt.Sprintf("⏰ 该客户的试甚期将于 **%s** 到期请及时跟进", expiryText), }, }, map[string]interface{}{ "tag": "hr", }, map[string]interface{}{ "tag": "note", "elements": []interface{}{ map[string]interface{}{ "tag": "plain_text", "content": fmt.Sprintf("发送时闎%s", time.Now().Format("2006-01-02 15:04:05")), }, }, }, }, }, } jsonData, err := json.Marshal(message) if err != nil { return fmt.Errorf("failed to marshal message: %v", err) } // Debug: Print the JSON being sent log.Printf("DEBUG: Sending Feishu message: %s", string(jsonData)) resp, err := http.Post(t.feishuWebhook, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to send request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("feishu API returned status: %d", resp.StatusCode) } return nil } // ExtractEndDate extracts the end date from trial period string for display func ExtractEndDate(trialStr string) string { if trialStr == "" { return "" } // Use regex to extract the end date re := regexp.MustCompile(`[~]\s*(.+)$`) matches := re.FindStringSubmatch(trialStr) if len(matches) > 1 { return strings.TrimSpace(matches[1]) } return trialStr }