simCardStatistics.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. package simCardQueue
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/hmac"
  6. "crypto/sha256"
  7. "encoding/base64"
  8. "encoding/json"
  9. "fmt"
  10. "fwh-toolbox-server/domain"
  11. "fwh-toolbox-server/repositories/mongo"
  12. "net/http"
  13. "net/url"
  14. "strconv"
  15. "time"
  16. "github.com/hibiken/asynq"
  17. )
  18. const taskTypeSimCardDailyReport = "SimCard:DailyReport"
  19. func calculateSimCardStatistics(date time.Time) (domain.SimCardDailyStatistics, error) {
  20. stats := domain.SimCardDailyStatistics{Date: date.Format(time.DateOnly)}
  21. err := mongo.StreamSimCardDailyUsage(date, func(total float64, previous *float64) error {
  22. accumulateSimCardStatistics(&stats, total, previous)
  23. return nil
  24. })
  25. return stats, err
  26. }
  27. func accumulateSimCardStatistics(stats *domain.SimCardDailyStatistics, total float64, previous *float64) {
  28. stats.CardCount++
  29. if total > stats.MaxTotalKB {
  30. stats.MaxTotalKB = total
  31. }
  32. if total > 200*1024 {
  33. stats.TotalOver200MB++
  34. }
  35. if total > 1024*1024 {
  36. stats.TotalOver1GB++
  37. }
  38. if total > 2*1024*1024 {
  39. stats.TotalOver2GB++
  40. }
  41. if previous == nil {
  42. return
  43. }
  44. increment := total - *previous
  45. // Monthly counters can reset at a cycle boundary. The new counter then
  46. // represents the usage accumulated since the reset.
  47. if increment < 0 {
  48. increment = total
  49. }
  50. stats.IncrementCardCount++
  51. if stats.MaxIncrementKB == nil || increment > *stats.MaxIncrementKB {
  52. stats.MaxIncrementKB = &increment
  53. }
  54. if increment > 20*1024 {
  55. stats.IncrementOver20MB++
  56. }
  57. if increment > 50*1024 {
  58. stats.IncrementOver50MB++
  59. }
  60. if increment > 200*1024 {
  61. stats.IncrementOver200MB++
  62. }
  63. }
  64. func (processor *SimCardProcessor) enqueueSimCardReportIfComplete(date string) error {
  65. if processor.webhook == "" {
  66. return nil
  67. }
  68. complete, err := mongo.IsSimCardRunComplete(date)
  69. if err != nil || !complete {
  70. return err
  71. }
  72. _, err = TaskClient.Enqueue(asynq.NewTask(taskTypeSimCardDailyReport, []byte(date),
  73. asynq.Queue("default"), asynq.MaxRetry(10), asynq.Timeout(3*time.Minute),
  74. asynq.TaskID("sim-card-daily-report-"+date), asynq.Retention(7*24*time.Hour)))
  75. if err == asynq.ErrTaskIDConflict {
  76. return nil
  77. }
  78. return err
  79. }
  80. func (processor *SimCardProcessor) handleSimCardDailyReport(ctx context.Context, task *asynq.Task) error {
  81. if processor.webhook == "" {
  82. return nil
  83. }
  84. dateStr := string(task.Payload())
  85. date, err := time.Parse(time.DateOnly, dateStr)
  86. if err != nil {
  87. return err
  88. }
  89. complete, err := mongo.IsSimCardRunComplete(dateStr)
  90. if err != nil || !complete {
  91. return err
  92. }
  93. stats, err := calculateSimCardStatistics(date)
  94. if err != nil {
  95. return err
  96. }
  97. if err := mongo.SaveSimCardDailyStatistics(stats); err != nil {
  98. return err
  99. }
  100. if err := sendSimCardStatistics(ctx, processor.webhook, processor.secret, stats); err != nil {
  101. return err
  102. }
  103. return mongo.MarkSimCardReportSent(dateStr)
  104. }
  105. func formatSimCardStatistics(stats domain.SimCardDailyStatistics) string {
  106. maxIncrement := "无可比数据"
  107. if stats.MaxIncrementKB != nil {
  108. maxIncrement = fmt.Sprintf("%.2f MB", *stats.MaxIncrementKB/1024)
  109. }
  110. return fmt.Sprintf("### SIM 卡流量日报(%s UTC)\n\n"+
  111. "总流量(%d 张卡,月累计):\n\n"+
  112. "- 超过 200 MB:%d 张\n- 超过 1 GB:%d 张\n- 超过 2 GB:%d 张\n- 最大总量:%.2f MB\n\n"+
  113. "增量流量(%d 张卡有前一日数据):\n\n"+
  114. "- 超过 20 MB:%d 张\n- 超过 50 MB:%d 张\n- 超过 200 MB:%d 张\n- 最大增量:%s",
  115. stats.Date, stats.CardCount, stats.TotalOver200MB, stats.TotalOver1GB, stats.TotalOver2GB,
  116. stats.MaxTotalKB/1024, stats.IncrementCardCount, stats.IncrementOver20MB,
  117. stats.IncrementOver50MB, stats.IncrementOver200MB, maxIncrement)
  118. }
  119. func sendSimCardStatistics(ctx context.Context, webhook, secret string, stats domain.SimCardDailyStatistics) error {
  120. return postSimCardStatistics(ctx, &http.Client{Timeout: 10 * time.Second}, webhook, secret, stats)
  121. }
  122. func postSimCardStatistics(ctx context.Context, client *http.Client, webhook, secret string, stats domain.SimCardDailyStatistics) error {
  123. if webhook == "" {
  124. return fmt.Errorf("SIM_CARD_DINGTALK_WEBHOOK is not configured")
  125. }
  126. u, err := url.Parse(webhook)
  127. if err != nil || u.Scheme != "https" || u.Host == "" {
  128. return fmt.Errorf("SIM_CARD_DINGTALK_WEBHOOK must be an HTTPS URL")
  129. }
  130. if secret != "" {
  131. timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
  132. mac := hmac.New(sha256.New, []byte(secret))
  133. _, _ = mac.Write([]byte(timestamp + "\n" + secret))
  134. query := u.Query()
  135. query.Set("timestamp", timestamp)
  136. query.Set("sign", base64.StdEncoding.EncodeToString(mac.Sum(nil)))
  137. u.RawQuery = query.Encode()
  138. }
  139. message := map[string]any{"msgtype": "markdown", "markdown": map[string]string{
  140. "title": "SIM 卡流量日报 " + stats.Date,
  141. "text": formatSimCardStatistics(stats),
  142. }}
  143. body, err := json.Marshal(message)
  144. if err != nil {
  145. return err
  146. }
  147. req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
  148. if err != nil {
  149. return err
  150. }
  151. req.Header.Set("Content-Type", "application/json")
  152. resp, err := client.Do(req)
  153. if err != nil {
  154. return fmt.Errorf("send SIM card statistics to DingTalk: %w", err)
  155. }
  156. defer resp.Body.Close()
  157. var result struct {
  158. ErrCode *int `json:"errcode"`
  159. ErrMsg string `json:"errmsg"`
  160. }
  161. if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
  162. return err
  163. }
  164. if resp.StatusCode != http.StatusOK || result.ErrCode == nil || *result.ErrCode != 0 {
  165. return fmt.Errorf("DingTalk rejected SIM card statistics: HTTP %d, code %v, %s", resp.StatusCode, result.ErrCode, result.ErrMsg)
  166. }
  167. return nil
  168. }