package handlers import ( "bytes" "encoding/json" "fmt" "log" "net/http" "strings" "time" "crm-go/internal/storage" "crm-go/models" ) type TrialPeriodHandler struct { storage storage.TrialPeriodStorage customerStorage storage.CustomerStorage feishuWebhook string } func NewTrialPeriodHandler(storage storage.TrialPeriodStorage, customerStorage storage.CustomerStorage, feishuWebhook string) *TrialPeriodHandler { return &TrialPeriodHandler{ storage: storage, customerStorage: customerStorage, feishuWebhook: feishuWebhook, } } // GetTrialPeriodsByCustomer returns all trial periods for a specific customer func (h *TrialPeriodHandler) GetTrialPeriodsByCustomer(w http.ResponseWriter, r *http.Request) { customerID := r.URL.Query().Get("customerId") if customerID == "" { http.Error(w, "Customer ID is required", http.StatusBadRequest) return } trialPeriods, err := h.storage.GetTrialPeriodsByCustomerID(customerID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "trialPeriods": trialPeriods, }) } // GetAllTrialPeriods returns all trial periods func (h *TrialPeriodHandler) GetAllTrialPeriods(w http.ResponseWriter, r *http.Request) { trialPeriods, err := h.storage.GetAllTrialPeriods() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "trialPeriods": trialPeriods, }) } // CreateTrialPeriod creates a new trial period func (h *TrialPeriodHandler) CreateTrialPeriod(w http.ResponseWriter, r *http.Request) { var req models.CreateTrialPeriodRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Parse start and end times startTime, err := time.Parse(time.RFC3339, req.StartTime) if err != nil { http.Error(w, "Invalid start time format", http.StatusBadRequest) return } endTime, err := time.Parse(time.RFC3339, req.EndTime) if err != nil { http.Error(w, "Invalid end time format", http.StatusBadRequest) return } trialPeriod := models.TrialPeriod{ CustomerID: req.CustomerID, StartTime: startTime, EndTime: endTime, CreatedAt: time.Now(), } createdPeriod, err := h.storage.CreateTrialPeriod(trialPeriod) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Send Feishu notification if h.feishuWebhook != "" { go h.sendTrialNotification(req.CustomerID, startTime, endTime) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(createdPeriod) } // UpdateTrialPeriod updates an existing trial period func (h *TrialPeriodHandler) UpdateTrialPeriod(w http.ResponseWriter, r *http.Request) { urlPath := r.URL.Path pathParts := strings.Split(urlPath, "/") if len(pathParts) < 4 { http.Error(w, "Invalid URL format", http.StatusBadRequest) return } id := pathParts[3] if idx := strings.Index(id, "?"); idx != -1 { id = id[:idx] } var req models.UpdateTrialPeriodRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Get existing trial period for notification existingPeriod, err := h.storage.GetTrialPeriodByID(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := h.storage.UpdateTrialPeriod(id, req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Send Feishu notification if updated if existingPeriod != nil && h.feishuWebhook != "" { var startTime, endTime time.Time if req.StartTime != nil { startTime, _ = time.Parse(time.RFC3339, *req.StartTime) } else { startTime = existingPeriod.StartTime } if req.EndTime != nil { endTime, _ = time.Parse(time.RFC3339, *req.EndTime) } else { endTime = existingPeriod.EndTime } go h.sendTrialNotification(existingPeriod.CustomerID, startTime, endTime) } w.WriteHeader(http.StatusOK) } // DeleteTrialPeriod deletes a trial period func (h *TrialPeriodHandler) DeleteTrialPeriod(w http.ResponseWriter, r *http.Request) { urlPath := r.URL.Path pathParts := strings.Split(urlPath, "/") if len(pathParts) < 4 { http.Error(w, "Invalid URL format", http.StatusBadRequest) return } id := pathParts[3] if idx := strings.Index(id, "?"); idx != -1 { id = id[:idx] } if err := h.storage.DeleteTrialPeriod(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } // sendTrialNotification sends a rich Feishu card notification func (h *TrialPeriodHandler) sendTrialNotification(customerID string, startTime, endTime time.Time) { if h.feishuWebhook == "" { return } // Calculate days until expiry now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) endDateOnly := time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 0, 0, 0, 0, endTime.Location()) daysUntilExpiry := int(endDateOnly.Sub(today).Hours() / 24) // Only send notification if expiring within 3 days if daysUntilExpiry < 0 || daysUntilExpiry > 3 { return } // Get customer name customerName := customerID customer, err := h.customerStorage.GetCustomerByID(customerID) if err == nil && customer != nil { customerName = customer.CustomerName } // Determine urgency level and message based on days until expiry var title, urgencyLevel, urgencyColor, expiryText string 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) } // Format trial period trialPeriod := fmt.Sprintf("%d/%d/%d %02d:%02d%d/%d/%d %02d:%02d", startTime.Year(), startTime.Month(), startTime.Day(), startTime.Hour(), startTime.Minute(), endTime.Year(), endTime.Month(), endTime.Day(), endTime.Hour(), endTime.Minute()) // 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 { log.Printf("Failed to marshal Feishu message: %v", err) return } log.Printf("DEBUG: Sending Feishu message from trial_period_handler: %s", string(jsonData)) resp, err := http.Post(h.feishuWebhook, "application/json", bytes.NewBuffer(jsonData)) if err != nil { log.Printf("Failed to send Feishu notification: %v", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("Feishu API returned status: %d", resp.StatusCode) return } log.Printf("Sent trial notification for customer: %s (expires in %d days)", customerID, daysUntilExpiry) }