crm/internal/handlers/customer_handler.go

471 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handlers
import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"crm-go/internal/storage"
"crm-go/models"
"github.com/google/uuid"
"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,
Screenshots: req.Screenshots,
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) UploadScreenshots(w http.ResponseWriter, r *http.Request) {
// Parse multipart form
err := r.ParseMultipartForm(32 << 20) // 32MB max
if err != nil {
http.Error(w, "Unable to parse form", http.StatusBadRequest)
return
}
files := r.MultipartForm.File["screenshots"]
if len(files) == 0 {
http.Error(w, "No files uploaded", http.StatusBadRequest)
return
}
// Ensure upload directory exists
uploadDir := "./frontend/uploads/screenshots"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
var filePaths []string
for _, fileHeader := range files {
file, err := fileHeader.Open()
if err != nil {
continue
}
defer file.Close()
// Generate unique filename
ext := filepath.Ext(fileHeader.Filename)
newFilename := uuid.New().String() + ext
filePath := filepath.Join(uploadDir, newFilename)
// Create file on disk
dst, err := os.Create(filePath)
if err != nil {
continue
}
defer dst.Close()
// Copy file content
if _, err := io.Copy(dst, file); err != nil {
continue
}
// Save relative path for frontend
relativePath := "/static/uploads/screenshots/" + newFilename
filePaths = append(filePaths, relativePath)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"filePaths": filePaths,
})
}
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)
}