369 lines
10 KiB
Go
369 lines
10 KiB
Go
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,
|
||
})
|
||
}
|