package handlers import ( "bytes" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "crm-go/internal/storage" "crm-go/models" ) type FollowUpHandler struct { storage storage.FollowUpStorage customerStorage storage.CustomerStorage feishuWebhook string } func NewFollowUpHandler(storage storage.FollowUpStorage, customerStorage storage.CustomerStorage, feishuWebhook string) *FollowUpHandler { return &FollowUpHandler{ storage: storage, customerStorage: customerStorage, feishuWebhook: feishuWebhook, } } func (h *FollowUpHandler) GetFollowUps(w http.ResponseWriter, r *http.Request) { page := 1 pageSize := 10 if pageStr := r.URL.Query().Get("page"); pageStr != "" { if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { page = p } } if pageSizeStr := r.URL.Query().Get("pageSize"); pageSizeStr != "" { if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 { pageSize = ps } } followUps, err := h.storage.GetAllFollowUps() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } total := len(followUps) totalPages := (total + pageSize - 1) / pageSize start := (page - 1) * pageSize end := start + pageSize if start >= total { followUps = []models.FollowUp{} } else if end > total { followUps = followUps[start:] } else { followUps = followUps[start:end] } response := map[string]interface{}{ "followUps": followUps, "total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func (h *FollowUpHandler) GetFollowUpByID(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] } followUp, err := h.storage.GetFollowUpByID(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if followUp == nil { http.Error(w, "Follow-up not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(followUp) } func (h *FollowUpHandler) CreateFollowUp(w http.ResponseWriter, r *http.Request) { var req models.CreateFollowUpRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Parse follow-up time followUpTime, err := time.Parse(time.RFC3339, req.FollowUpTime) if err != nil { http.Error(w, "Invalid follow-up time format", http.StatusBadRequest) return } followUp := models.FollowUp{ CustomerName: req.CustomerName, DealStatus: req.DealStatus, CustomerLevel: req.CustomerLevel, Industry: req.Industry, FollowUpTime: followUpTime, CreatedAt: time.Now(), NotificationSent: false, } if err := h.storage.CreateFollowUp(followUp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(followUp) } func (h *FollowUpHandler) UpdateFollowUp(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.UpdateFollowUpRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if err := h.storage.UpdateFollowUp(id, req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } func (h *FollowUpHandler) DeleteFollowUp(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.DeleteFollowUp(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } // SendFeishuNotification sends a notification to Feishu func (h *FollowUpHandler) SendFeishuNotification(customerName string) error { if h.feishuWebhook == "" { return fmt.Errorf("Feishu webhook URL not configured") } // Create Feishu 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": "客户跟进提醒", }, "template": "blue", }, "elements": []map[string]interface{}{ { "tag": "div", "text": map[string]interface{}{ "tag": "lark_md", "content": fmt.Sprintf(" 请及时跟进 \"%s\"", customerName), }, }, }, }, } jsonData, err := json.Marshal(message) if err != nil { return err } resp, err := http.Post(h.feishuWebhook, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("Feishu API returned status code: %d", resp.StatusCode) } return nil } // CheckAndSendNotifications checks for pending notifications and sends them func (h *FollowUpHandler) CheckAndSendNotifications() error { pendingFollowUps, err := h.storage.GetPendingNotifications() if err != nil { return err } for _, followUp := range pendingFollowUps { // Send Feishu notification if err := h.SendFeishuNotification(followUp.CustomerName); err != nil { fmt.Printf("Error sending notification for follow-up %s: %v\n", followUp.ID, err) continue } // Mark notification as sent if err := h.storage.MarkNotificationSent(followUp.ID); err != nil { fmt.Printf("Error marking notification as sent for follow-up %s: %v\n", followUp.ID, err) } } return nil } // GetCustomerList returns a list of customers with id and name func (h *FollowUpHandler) GetCustomerList(w http.ResponseWriter, r *http.Request) { customers, err := h.customerStorage.GetAllCustomers() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Printf("DEBUG: Total customers from storage: %d\n", len(customers)) // Create a list of customer objects with id and name type CustomerInfo struct { ID string `json:"id"` CustomerName string `json:"customerName"` } // Use a map to deduplicate customers by name customerMap := make(map[string]CustomerInfo) for _, customer := range customers { if customer.CustomerName != "" { // Only keep the first occurrence of each customer name if _, exists := customerMap[customer.CustomerName]; !exists { customerMap[customer.CustomerName] = CustomerInfo{ ID: customer.ID, CustomerName: customer.CustomerName, } fmt.Printf("DEBUG: Added customer: ID=%s, Name=%s\n", customer.ID, customer.CustomerName) } } } // Convert map to slice var customerList []CustomerInfo for _, customer := range customerMap { customerList = append(customerList, customer) } fmt.Printf("DEBUG: Total unique customer list items: %d\n", len(customerList)) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "customers": customerList, }) }