crm/internal/handlers/trial_period_handler.go
2026-01-16 16:28:50 +08:00

369 lines
10 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/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,
IsTrial: req.IsTrial,
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
}
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)
}
// GetTrialCustomerList returns a list of unique customer names from trial periods
func (h *TrialPeriodHandler) GetTrialCustomerList(w http.ResponseWriter, r *http.Request) {
trialPeriods, err := h.storage.GetAllTrialPeriods()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Create a set of unique customer IDs
customerIDs := make(map[string]bool)
for _, period := range trialPeriods {
if period.CustomerID != "" {
customerIDs[period.CustomerID] = true
}
}
// Get unique customer names (only include customers that exist in storage)
seenNames := make(map[string]bool)
var customerNames []string
for customerID := range customerIDs {
// Try to get customer name from customer storage
customer, err := h.customerStorage.GetCustomerByID(customerID)
if err == nil && customer != nil && customer.CustomerName != "" {
customerName := customer.CustomerName
// Add to list if not seen before
if !seenNames[customerName] {
customerNames = append(customerNames, customerName)
seenNames[customerName] = true
}
}
// Skip customers that don't exist in storage (don't show ID as name)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"customerNames": customerNames,
})
}