custom_channel_time_pricing.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. package service
  2. import (
  3. "fmt"
  4. "math"
  5. "sort"
  6. "strings"
  7. "sync"
  8. "time"
  9. )
  10. var channelTimePricingLocations sync.Map
  11. type parsedChannelTimePeriod struct {
  12. start int
  13. end int
  14. multiplier float64
  15. }
  16. // validateChannelTimePricing 校验分时倍率配置。nil 或空 periods 表示未启用。
  17. func validateChannelTimePricing(config *ChannelTimePricing) error {
  18. if config == nil || len(config.Periods) == 0 {
  19. return nil
  20. }
  21. if _, err := loadChannelTimePricingLocation(config.Timezone); err != nil {
  22. return fmt.Errorf("timezone: %w", err)
  23. }
  24. _, err := parseChannelTimePeriods(config.Periods)
  25. return err
  26. }
  27. // MultiplierAt 返回 at 对应的分时倍率。无配置或脏配置均安全降级为 1。
  28. func (config *ChannelTimePricing) MultiplierAt(at time.Time) float64 {
  29. if config == nil || len(config.Periods) == 0 || at.IsZero() {
  30. return 1.0
  31. }
  32. if err := validateChannelTimePricing(config); err != nil {
  33. return 1.0
  34. }
  35. location, err := loadChannelTimePricingLocation(config.Timezone)
  36. if err != nil {
  37. return 1.0
  38. }
  39. periods, err := parseChannelTimePeriods(config.Periods)
  40. if err != nil {
  41. return 1.0
  42. }
  43. local := at.In(location)
  44. if config.WeekdaysOnly && (local.Weekday() == time.Saturday || local.Weekday() == time.Sunday) {
  45. return 1.0
  46. }
  47. second := local.Hour()*60*60 + local.Minute()*60 + local.Second()
  48. for _, period := range periods {
  49. if second >= period.start && second < period.end {
  50. return period.multiplier
  51. }
  52. }
  53. return 1.0
  54. }
  55. func loadChannelTimePricingLocation(name string) (*time.Location, error) {
  56. if strings.TrimSpace(name) == "" {
  57. return nil, fmt.Errorf("timezone is required")
  58. }
  59. if name == "Local" {
  60. return nil, fmt.Errorf("local is not a supported timezone")
  61. }
  62. if cached, ok := channelTimePricingLocations.Load(name); ok {
  63. location, valid := cached.(*time.Location)
  64. if valid && location != nil {
  65. return location, nil
  66. }
  67. channelTimePricingLocations.Delete(name)
  68. }
  69. location, err := time.LoadLocation(name)
  70. if err != nil {
  71. return nil, err
  72. }
  73. actual, _ := channelTimePricingLocations.LoadOrStore(name, location)
  74. actualLocation, ok := actual.(*time.Location)
  75. if !ok || actualLocation == nil {
  76. return nil, fmt.Errorf("invalid cached timezone %q", name)
  77. }
  78. return actualLocation, nil
  79. }
  80. func parseChannelTime(value string, end bool) (int, error) {
  81. if end && (value == "00:00" || value == "00:00:00") {
  82. return 24 * 60 * 60, nil
  83. }
  84. layout := "15:04:05"
  85. if len(value) == len("15:04") {
  86. layout = "15:04"
  87. }
  88. parsed, err := time.Parse(layout, value)
  89. if err != nil || parsed.Format(layout) != value {
  90. return 0, fmt.Errorf("time %q must use HH:mm or HH:mm:ss format", value)
  91. }
  92. return parsed.Hour()*60*60 + parsed.Minute()*60 + parsed.Second(), nil
  93. }
  94. func parseChannelTimePeriods(periods []ChannelTimePricingPeriod) ([]parsedChannelTimePeriod, error) {
  95. parsed := make([]parsedChannelTimePeriod, 0, len(periods))
  96. for _, period := range periods {
  97. if math.IsNaN(period.Multiplier) || math.IsInf(period.Multiplier, 0) || period.Multiplier <= 0 {
  98. return nil, fmt.Errorf("multiplier must be finite and greater than 0")
  99. }
  100. if period.Multiplier < 0.01 {
  101. return nil, fmt.Errorf("multiplier must be at least 0.01")
  102. }
  103. scaled := period.Multiplier * 100
  104. if math.IsNaN(scaled) || math.IsInf(scaled, 0) {
  105. return nil, fmt.Errorf("multiplier must remain finite when scaled")
  106. }
  107. if math.Abs(scaled-math.Round(scaled)) > 1e-9 {
  108. return nil, fmt.Errorf("multiplier must have at most two decimal places")
  109. }
  110. start, err := parseChannelTime(period.StartTime, false)
  111. if err != nil {
  112. return nil, err
  113. }
  114. end, err := parseChannelTime(period.EndTime, true)
  115. if err != nil {
  116. return nil, err
  117. }
  118. if period.StartTime == period.EndTime || start >= end {
  119. return nil, fmt.Errorf("start time must be before end time")
  120. }
  121. parsed = append(parsed, parsedChannelTimePeriod{start: start, end: end, multiplier: period.Multiplier})
  122. }
  123. sort.Slice(parsed, func(i, j int) bool {
  124. return parsed[i].start < parsed[j].start
  125. })
  126. for i := 1; i < len(parsed); i++ {
  127. if parsed[i].start < parsed[i-1].end {
  128. return nil, fmt.Errorf("time pricing periods overlap")
  129. }
  130. }
  131. return parsed, nil
  132. }