package handlers import ( "bytes" "encoding/csv" "encoding/json" "fmt" "io" "log" "net/http" "strconv" "strings" "time" "crm-go/internal/storage" "crm-go/models" "github.com/xuri/excelize/v2" ) type CustomerHandler struct { storage storage.CustomerStorage feishuWebhook string } func NewCustomerHandler(storage storage.CustomerStorage, feishuWebhook string) *CustomerHandler { return &CustomerHandler{ storage: storage, feishuWebhook: feishuWebhook, } } func (h *CustomerHandler) GetCustomers(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 } } customers, err := h.storage.GetAllCustomers() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } total := len(customers) totalPages := (total + pageSize - 1) / pageSize start := (page - 1) * pageSize end := start + pageSize if start >= total { customers = []models.Customer{} } else if end > total { customers = customers[start:] } else { customers = customers[start:end] } response := map[string]interface{}{ "customers": customers, "total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func (h *CustomerHandler) GetCustomerByID(w http.ResponseWriter, r *http.Request) { // Extract customer ID from URL path urlPath := r.URL.Path // The path should be /api/customers/{id} pathParts := strings.Split(urlPath, "/") if len(pathParts) < 4 { http.Error(w, "Invalid URL format", http.StatusBadRequest) return } id := pathParts[3] // Get the ID from the path // Remove query parameters if any if idx := strings.Index(id, "?"); idx != -1 { id = id[:idx] } customer, err := h.storage.GetCustomerByID(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if customer == nil { http.Error(w, "Customer not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(customer) } func (h *CustomerHandler) CreateCustomer(w http.ResponseWriter, r *http.Request) { var req models.CreateCustomerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } customer := models.Customer{ CustomerName: req.CustomerName, IntendedProduct: req.IntendedProduct, Version: req.Version, Description: req.Description, Solution: req.Solution, Type: req.Type, Module: req.Module, StatusProgress: req.StatusProgress, Reporter: req.Reporter, CreatedAt: time.Now(), } if err := h.storage.CreateCustomer(customer); 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(customer) } func (h *CustomerHandler) UpdateCustomer(w http.ResponseWriter, r *http.Request) { // Extract customer ID from URL path urlPath := r.URL.Path // The path should be /api/customers/{id} pathParts := strings.Split(urlPath, "/") if len(pathParts) < 4 { http.Error(w, "Invalid URL format", http.StatusBadRequest) return } id := pathParts[3] // Get the ID from the path // Remove query parameters if any if idx := strings.Index(id, "?"); idx != -1 { id = id[:idx] } var req models.UpdateCustomerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Get customer info for notification customer, err := h.storage.GetCustomerByID(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := h.storage.UpdateCustomer(id, req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Send Feishu notification if reporter (trial period) is updated if req.Reporter != nil && *req.Reporter != "" && h.feishuWebhook != "" && customer != nil { go h.sendTrialNotification(customer.CustomerName, *req.Reporter) } w.WriteHeader(http.StatusOK) } func (h *CustomerHandler) DeleteCustomer(w http.ResponseWriter, r *http.Request) { // Extract customer ID from URL path urlPath := r.URL.Path // The path should be /api/customers/{id} pathParts := strings.Split(urlPath, "/") if len(pathParts) < 4 { http.Error(w, "Invalid URL format", http.StatusBadRequest) return } id := pathParts[3] // Get the ID from the path // Remove query parameters if any if idx := strings.Index(id, "?"); idx != -1 { id = id[:idx] } if err := h.storage.DeleteCustomer(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } func (h *CustomerHandler) ImportCustomers(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(10 << 20) // 10 MB if err != nil { http.Error(w, "Unable to parse form", http.StatusBadRequest) return } file, handler, err := r.FormFile("file") if err != nil { http.Error(w, "Unable to get file", http.StatusBadRequest) return } defer file.Close() // Check file extension filename := handler.Filename isExcel := strings.HasSuffix(strings.ToLower(filename), ".xlsx") importedCount := 0 duplicateCount := 0 if isExcel { // Parse as Excel file f, err := excelize.OpenReader(file) if err != nil { http.Error(w, "Unable to open Excel file", http.StatusBadRequest) return } defer f.Close() // Get the first sheet sheets := f.GetSheetList() if len(sheets) == 0 { http.Error(w, "No sheets found in Excel file", http.StatusBadRequest) return } // Read all rows from the first sheet rows, err := f.GetRows(sheets[0]) if err != nil { http.Error(w, "Unable to read Excel file", http.StatusBadRequest) return } // Skip header row if exists for i, row := range rows { if i == 0 { continue // Skip header row } if len(row) < 2 { continue // Skip rows with insufficient data } customer := models.Customer{ ID: "", // Will be generated by the storage CreatedAt: time.Now(), CustomerName: getValue(row, 0), IntendedProduct: getValue(row, 1), Version: getValue(row, 2), Description: getValue(row, 3), Solution: getValue(row, 4), Type: getValue(row, 5), Module: getValue(row, 6), StatusProgress: getValue(row, 7), Reporter: "", } // Check for duplicate exists, err := h.storage.CustomerExists(customer) if err != nil { http.Error(w, "Error checking for duplicate customer", http.StatusInternalServerError) return } if exists { duplicateCount++ continue // Skip duplicate } if err := h.storage.CreateCustomer(customer); err != nil { http.Error(w, "Error saving customer", http.StatusInternalServerError) return } importedCount++ } } else { // Parse as CSV content, err := io.ReadAll(file) if err != nil { http.Error(w, "Unable to read file", http.StatusBadRequest) return } reader := csv.NewReader(strings.NewReader(string(content))) records, err := reader.ReadAll() if err != nil { http.Error(w, "Unable to parse CSV file", http.StatusBadRequest) return } // Skip header row if exists for i, row := range records { if i == 0 { continue // Skip header row } if len(row) < 2 { continue // Skip rows with insufficient data } customer := models.Customer{ ID: "", // Will be generated by the storage CreatedAt: time.Now(), CustomerName: getValue(row, 0), IntendedProduct: getValue(row, 1), Version: getValue(row, 2), Description: getValue(row, 3), Solution: getValue(row, 4), Type: getValue(row, 5), Module: getValue(row, 6), StatusProgress: getValue(row, 7), Reporter: "", } // Check for duplicate exists, err := h.storage.CustomerExists(customer) if err != nil { http.Error(w, "Error checking for duplicate customer", http.StatusInternalServerError) return } if exists { duplicateCount++ continue // Skip duplicate } if err := h.storage.CreateCustomer(customer); err != nil { http.Error(w, "Error saving customer", http.StatusInternalServerError) return } importedCount++ } } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Customers imported successfully", "importedCount": importedCount, "duplicateCount": duplicateCount, }) } func getValue(row []string, index int) string { if index < len(row) { return row[index] } return "" } // sendTrialNotification sends a Feishu notification when trial period is set/updated func (h *CustomerHandler) sendTrialNotification(customerName, trialPeriod string) { if h.feishuWebhook == "" { return } message := map[string]interface{}{ "msg_type": "text", "content": map[string]string{ "text": fmt.Sprintf("温馨提示:%s客户于明日试用到期,请及时跟进!\n试用时间:%s", customerName, trialPeriod), }, } 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", customerName) }