310 lines
8.8 KiB
Go
310 lines
8.8 KiB
Go
package services
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// TrialExpiryChecker checks for trial expiry and sends notifications
|
||
type TrialExpiryChecker struct {
|
||
feishuWebhook string
|
||
}
|
||
|
||
// NewTrialExpiryChecker creates a new trial expiry checker
|
||
func NewTrialExpiryChecker(feishuWebhook string) *TrialExpiryChecker {
|
||
return &TrialExpiryChecker{
|
||
feishuWebhook: feishuWebhook,
|
||
}
|
||
}
|
||
|
||
// Customer represents a customer record
|
||
type Customer struct {
|
||
ID string `json:"id"`
|
||
CreatedAt string `json:"createdAt"`
|
||
CustomerName string `json:"customerName"`
|
||
IntendedProduct string `json:"intendedProduct"`
|
||
Version string `json:"version"`
|
||
Description string `json:"description"`
|
||
Solution string `json:"solution"`
|
||
Type string `json:"type"`
|
||
Module string `json:"module"`
|
||
StatusProgress string `json:"statusProgress"`
|
||
Reporter string `json:"reporter"` // Now used for trial period
|
||
}
|
||
|
||
// TrialPeriod represents a trial period record
|
||
type TrialPeriod struct {
|
||
ID string `json:"id"`
|
||
CustomerName string `json:"customerName"`
|
||
StartTime time.Time `json:"startTime"`
|
||
EndTime time.Time `json:"endTime"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
}
|
||
|
||
// ParseTrialPeriod parses the trial period string
|
||
// Supports formats like: "2026/1/5~2026/1/16", "2026/1/5~2026/1/16", "2026-1-5~2026-1-16"
|
||
func ParseTrialPeriod(trialStr string) (startDate, endDate time.Time, err error) {
|
||
if trialStr == "" {
|
||
return time.Time{}, time.Time{}, fmt.Errorf("empty trial period")
|
||
}
|
||
|
||
// Replace different separators with standard ~
|
||
trialStr = strings.ReplaceAll(trialStr, "~", "~")
|
||
trialStr = strings.ReplaceAll(trialStr, " ", "")
|
||
|
||
// Split by ~
|
||
parts := strings.Split(trialStr, "~")
|
||
if len(parts) != 2 {
|
||
return time.Time{}, time.Time{}, fmt.Errorf("invalid trial period format")
|
||
}
|
||
|
||
// Parse dates - support multiple formats
|
||
layouts := []string{
|
||
"2006/1/2",
|
||
"2006-1-2",
|
||
"2006/01/02",
|
||
"2006-01-02",
|
||
}
|
||
|
||
var parseErr error
|
||
for _, layout := range layouts {
|
||
startDate, parseErr = time.Parse(layout, strings.TrimSpace(parts[0]))
|
||
if parseErr == nil {
|
||
break
|
||
}
|
||
}
|
||
if parseErr != nil {
|
||
return time.Time{}, time.Time{}, fmt.Errorf("failed to parse start date: %v", parseErr)
|
||
}
|
||
|
||
for _, layout := range layouts {
|
||
endDate, parseErr = time.Parse(layout, strings.TrimSpace(parts[1]))
|
||
if parseErr == nil {
|
||
break
|
||
}
|
||
}
|
||
if parseErr != nil {
|
||
return time.Time{}, time.Time{}, fmt.Errorf("failed to parse end date: %v", parseErr)
|
||
}
|
||
|
||
return startDate, endDate, nil
|
||
}
|
||
|
||
// CheckAndNotify checks for customers whose trial expires soon and sends notifications
|
||
func (t *TrialExpiryChecker) CheckAndNotify(customers []Customer) error {
|
||
if t.feishuWebhook == "" {
|
||
log.Println("Feishu webhook not configured, skipping trial expiry notifications")
|
||
return nil
|
||
}
|
||
|
||
now := time.Now()
|
||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||
|
||
for _, customer := range customers {
|
||
if customer.Reporter == "" {
|
||
continue
|
||
}
|
||
|
||
_, endDate, err := ParseTrialPeriod(customer.Reporter)
|
||
if err != nil {
|
||
// Skip invalid trial periods
|
||
continue
|
||
}
|
||
|
||
// Check if trial expires within 3 days
|
||
endDateOnly := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, endDate.Location())
|
||
daysUntilExpiry := int(endDateOnly.Sub(today).Hours() / 24)
|
||
|
||
// Send notification for trials expiring today, tomorrow, or within 3 days
|
||
if daysUntilExpiry >= 0 && daysUntilExpiry <= 3 {
|
||
err := t.sendExpiryNotification(customer.CustomerName, customer.Reporter, daysUntilExpiry)
|
||
if err != nil {
|
||
log.Printf("Failed to send expiry notification for %s: %v", customer.CustomerName, err)
|
||
} else {
|
||
log.Printf("Sent trial expiry notification for customer: %s (expires in %d days)", customer.CustomerName, daysUntilExpiry)
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// CheckTrialPeriodsAndNotify checks trial periods and sends notifications
|
||
func (t *TrialExpiryChecker) CheckTrialPeriodsAndNotify(trialPeriods []TrialPeriod, customersMap map[string]string) error {
|
||
if t.feishuWebhook == "" {
|
||
log.Println("Feishu webhook not configured, skipping trial expiry notifications")
|
||
return nil
|
||
}
|
||
|
||
now := time.Now()
|
||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||
|
||
for _, period := range trialPeriods {
|
||
endDateOnly := time.Date(period.EndTime.Year(), period.EndTime.Month(), period.EndTime.Day(), 0, 0, 0, 0, period.EndTime.Location())
|
||
daysUntilExpiry := int(endDateOnly.Sub(today).Hours() / 24)
|
||
|
||
// Send notification for trials expiring today, tomorrow, or within 3 days
|
||
if daysUntilExpiry >= 0 && daysUntilExpiry <= 3 {
|
||
customerName := period.CustomerName
|
||
if customerName == "" {
|
||
customerName = "未知客户"
|
||
}
|
||
|
||
trialPeriodStr := fmt.Sprintf("%s~%s",
|
||
period.StartTime.Format("2006/1/2 15:04"),
|
||
period.EndTime.Format("2006/1/2 15:04"))
|
||
|
||
err := t.sendExpiryNotification(customerName, trialPeriodStr, daysUntilExpiry)
|
||
if err != nil {
|
||
log.Printf("Failed to send expiry notification for %s: %v", customerName, err)
|
||
} else {
|
||
log.Printf("Sent trial expiry notification for customer: %s (expires in %d days)", customerName, daysUntilExpiry)
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// sendExpiryNotification sends a rich Feishu card notification for trial expiry
|
||
func (t *TrialExpiryChecker) sendExpiryNotification(customerName, trialPeriod string, daysUntilExpiry int) error {
|
||
var title, urgencyLevel, urgencyColor, expiryText string
|
||
|
||
// Determine urgency level and message based on days until expiry
|
||
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)
|
||
}
|
||
|
||
// 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 {
|
||
return fmt.Errorf("failed to marshal message: %v", err)
|
||
}
|
||
|
||
// Debug: Print the JSON being sent
|
||
log.Printf("DEBUG: Sending Feishu message: %s", string(jsonData))
|
||
|
||
resp, err := http.Post(t.feishuWebhook, "application/json", bytes.NewBuffer(jsonData))
|
||
if err != nil {
|
||
return fmt.Errorf("failed to send request: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return fmt.Errorf("feishu API returned status: %d", resp.StatusCode)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ExtractEndDate extracts the end date from trial period string for display
|
||
func ExtractEndDate(trialStr string) string {
|
||
if trialStr == "" {
|
||
return ""
|
||
}
|
||
|
||
// Use regex to extract the end date
|
||
re := regexp.MustCompile(`[~~]\s*(.+)$`)
|
||
matches := re.FindStringSubmatch(trialStr)
|
||
if len(matches) > 1 {
|
||
return strings.TrimSpace(matches[1])
|
||
}
|
||
|
||
return trialStr
|
||
}
|