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 (optional - only required for 数据闭环) var startTime, endTime time.Time var parseErr error if req.StartTime != "" { startTime, parseErr = time.Parse(time.RFC3339, req.StartTime) if parseErr != nil { http.Error(w, "Invalid start time format", http.StatusBadRequest) return } } if req.EndTime != "" { endTime, parseErr = time.Parse(time.RFC3339, req.EndTime) if parseErr != nil { http.Error(w, "Invalid end time format", http.StatusBadRequest) return } } trialPeriod := models.TrialPeriod{ CustomerName: req.CustomerName, Source: req.Source, IntendedProduct: req.IntendedProduct, DealStatus: req.DealStatus, StartTime: startTime, EndTime: endTime, IsTrial: req.IsTrial, CreatedAt: time.Now(), } // Check if customer name already exists existing, err := h.storage.GetTrialPeriodsByCustomerID(req.CustomerName) if err == nil && len(existing) > 0 { http.Error(w, fmt.Sprintf("客户名称 '%s' 已存在,请勿重复添加", req.CustomerName), http.StatusConflict) return } 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.CustomerName, 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.CustomerName, 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(customerName 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 } // customerName is already passed as parameter, no need to lookup // Just use it directly for notification // 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 } 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)", customerName, daysUntilExpiry) } // GetTrialCustomerList returns a list of unique customer names from trial_periods // This is the data source for dropdown lists in 每周进度 and 客户跟进 func (h *TrialPeriodHandler) GetTrialCustomerList(w http.ResponseWriter, r *http.Request) { // Get all trial periods trialPeriods, err := h.storage.GetAllTrialPeriods() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Get unique customer names from trial periods seenNames := make(map[string]bool) var customerNames []string for _, period := range trialPeriods { if period.CustomerName != "" && !seenNames[period.CustomerName] { customerNames = append(customerNames, period.CustomerName) seenNames[period.CustomerName] = true } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "customerNames": customerNames, }) }