diff --git a/backend/internal/handler/admin/channel_handler.go b/backend/internal/handler/admin/channel_handler.go index dc0e5b07e2..c1b3507aef 100644 --- a/backend/internal/handler/admin/channel_handler.go +++ b/backend/internal/handler/admin/channel_handler.go @@ -74,8 +74,9 @@ type channelModelPricingRequest struct { } type channelTimePricingRequest struct { - Timezone string `json:"timezone"` - Periods []channelTimePricingPeriodRequest `json:"periods"` + Timezone string `json:"timezone"` + WeekdaysOnly bool `json:"weekdays_only"` + Periods []channelTimePricingPeriodRequest `json:"periods"` } type channelTimePricingPeriodRequest struct { @@ -144,8 +145,9 @@ type channelModelPricingResponse struct { } type channelTimePricingResponse struct { - Timezone string `json:"timezone"` - Periods []channelTimePricingPeriodResponse `json:"periods"` + Timezone string `json:"timezone"` + WeekdaysOnly bool `json:"weekdays_only"` + Periods []channelTimePricingPeriodResponse `json:"periods"` } type channelTimePricingPeriodResponse struct { @@ -282,7 +284,11 @@ func timePricingToResponse(value *service.ChannelTimePricing) *channelTimePricin Multiplier: period.Multiplier, }) } - return &channelTimePricingResponse{Timezone: value.Timezone, Periods: periods} + return &channelTimePricingResponse{ + Timezone: value.Timezone, + WeekdaysOnly: value.WeekdaysOnly, + Periods: periods, + } } func intervalToResponse(iv service.PricingInterval) pricingIntervalResponse { @@ -374,7 +380,11 @@ func timePricingRequestToService(value *channelTimePricingRequest) *service.Chan Multiplier: period.Multiplier, }) } - return &service.ChannelTimePricing{Timezone: value.Timezone, Periods: periods} + return &service.ChannelTimePricing{ + Timezone: value.Timezone, + WeekdaysOnly: value.WeekdaysOnly, + Periods: periods, + } } func accountStatsPricingRuleRequestToService(r accountStatsPricingRuleRequest) service.AccountStatsPricingRule { diff --git a/backend/internal/handler/admin/channel_handler_test.go b/backend/internal/handler/admin/channel_handler_test.go index 75aec7db31..2da7e02d00 100644 --- a/backend/internal/handler/admin/channel_handler_test.go +++ b/backend/internal/handler/admin/channel_handler_test.go @@ -426,7 +426,8 @@ func TestPricingRequestToService_TimePricing(t *testing.T) { Models: []string{"gpt-5"}, BillingMode: "token", TimePricing: &channelTimePricingRequest{ - Timezone: "Asia/Shanghai", + Timezone: "Asia/Shanghai", + WeekdaysOnly: true, Periods: []channelTimePricingPeriodRequest{{ StartTime: "09:00", EndTime: "12:00", Multiplier: 2, }}, @@ -435,6 +436,7 @@ func TestPricingRequestToService_TimePricing(t *testing.T) { got := pricingRequestToService([]channelModelPricingRequest{req}, true) require.Equal(t, "Asia/Shanghai", got[0].TimePricing.Timezone) + require.True(t, got[0].TimePricing.WeekdaysOnly) require.Equal(t, 2.0, got[0].TimePricing.Periods[0].Multiplier) } @@ -483,7 +485,8 @@ func TestPricingToResponse_TimePricing(t *testing.T) { got := pricingToResponse(&service.ChannelModelPricing{ BillingMode: service.BillingModeToken, TimePricing: &service.ChannelTimePricing{ - Timezone: "Asia/Shanghai", + Timezone: "Asia/Shanghai", + WeekdaysOnly: true, Periods: []service.ChannelTimePricingPeriod{{ StartTime: "14:00", EndTime: "18:00", Multiplier: 1.25, }}, @@ -492,6 +495,7 @@ func TestPricingToResponse_TimePricing(t *testing.T) { require.NotNil(t, got.TimePricing) require.Equal(t, "Asia/Shanghai", got.TimePricing.Timezone) + require.True(t, got.TimePricing.WeekdaysOnly) require.Equal(t, 1.25, got.TimePricing.Periods[0].Multiplier) } diff --git a/backend/internal/service/channel.go b/backend/internal/service/channel.go index 0fe4e3b07c..3e300edf99 100644 --- a/backend/internal/service/channel.go +++ b/backend/internal/service/channel.go @@ -109,8 +109,9 @@ type ChannelModelPricing struct { // ChannelTimePricing 渠道模型定价的分时倍率配置。 type ChannelTimePricing struct { - Timezone string `json:"timezone"` - Periods []ChannelTimePricingPeriod `json:"periods"` + Timezone string `json:"timezone"` + WeekdaysOnly bool `json:"weekdays_only,omitempty"` + Periods []ChannelTimePricingPeriod `json:"periods"` } // ChannelTimePricingPeriod 是秒级的左闭右开分时倍率区间,并兼容历史 HH:mm 数据。 @@ -216,7 +217,10 @@ func (p ChannelModelPricing) Clone() ChannelModelPricing { copy(cp.Intervals, p.Intervals) } if p.TimePricing != nil { - cp.TimePricing = &ChannelTimePricing{Timezone: p.TimePricing.Timezone} + cp.TimePricing = &ChannelTimePricing{ + Timezone: p.TimePricing.Timezone, + WeekdaysOnly: p.TimePricing.WeekdaysOnly, + } if p.TimePricing.Periods != nil { cp.TimePricing.Periods = append([]ChannelTimePricingPeriod(nil), p.TimePricing.Periods...) } diff --git a/backend/internal/service/channel_test.go b/backend/internal/service/channel_test.go index 19e45a02db..42c2c46448 100644 --- a/backend/internal/service/channel_test.go +++ b/backend/internal/service/channel_test.go @@ -198,7 +198,8 @@ func TestChannelModelPricingClone(t *testing.T) { {MinTokens: 0, TierLabel: "tier1"}, }, TimePricing: &ChannelTimePricing{ - Timezone: "Asia/Shanghai", + Timezone: "Asia/Shanghai", + WeekdaysOnly: true, Periods: []ChannelTimePricingPeriod{{ StartTime: "09:00", EndTime: "12:00", @@ -217,9 +218,11 @@ func TestChannelModelPricingClone(t *testing.T) { require.Equal(t, "tier1", original.Intervals[0].TierLabel) cloned.TimePricing.Timezone = "America/New_York" + cloned.TimePricing.WeekdaysOnly = false cloned.TimePricing.Periods[0].StartTime = "10:00" cloned.TimePricing.Periods[0].Multiplier = 3 require.Equal(t, "Asia/Shanghai", original.TimePricing.Timezone) + require.True(t, original.TimePricing.WeekdaysOnly) require.Equal(t, "09:00", original.TimePricing.Periods[0].StartTime) require.Equal(t, 2.0, original.TimePricing.Periods[0].Multiplier) } diff --git a/backend/internal/service/custom_channel_time_pricing.go b/backend/internal/service/custom_channel_time_pricing.go index 759eac987e..5b7ce8eb3d 100644 --- a/backend/internal/service/custom_channel_time_pricing.go +++ b/backend/internal/service/custom_channel_time_pricing.go @@ -47,6 +47,9 @@ func (config *ChannelTimePricing) MultiplierAt(at time.Time) float64 { } local := at.In(location) + if config.WeekdaysOnly && (local.Weekday() == time.Saturday || local.Weekday() == time.Sunday) { + return 1.0 + } second := local.Hour()*60*60 + local.Minute()*60 + local.Second() for _, period := range periods { if second >= period.start && second < period.end { diff --git a/backend/internal/service/custom_channel_time_pricing_test.go b/backend/internal/service/custom_channel_time_pricing_test.go index f1038af0d3..f36c19b7c9 100644 --- a/backend/internal/service/custom_channel_time_pricing_test.go +++ b/backend/internal/service/custom_channel_time_pricing_test.go @@ -95,6 +95,30 @@ func TestChannelTimePricingMultiplierAt(t *testing.T) { require.Equal(t, 2.0, newYork.MultiplierAt(at)) } +func TestChannelTimePricingMultiplierAtWeekdaysOnly(t *testing.T) { + config := &ChannelTimePricing{ + Timezone: "Asia/Shanghai", + WeekdaysOnly: true, + Periods: onePeriod(), + } + + tests := []struct { + name string + at time.Time + want float64 + }{ + {name: "Monday in configured timezone", at: time.Date(2026, 6, 29, 1, 0, 0, 0, time.UTC), want: 2}, + {name: "Saturday in configured timezone", at: time.Date(2026, 7, 4, 1, 0, 0, 0, time.UTC), want: 1}, + {name: "Sunday in configured timezone", at: time.Date(2026, 7, 5, 1, 0, 0, 0, time.UTC), want: 1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, config.MultiplierAt(tt.at)) + }) + } +} + func TestChannelTimePricingMultiplierAtSecondPrecision(t *testing.T) { config := timeConfig(ChannelTimePricingPeriod{StartTime: "09:00:30", EndTime: "09:00:45", Multiplier: 2}) shanghai, err := time.LoadLocation("Asia/Shanghai") diff --git a/frontend/src/api/admin/channels.ts b/frontend/src/api/admin/channels.ts index 9ce50bcd0a..94967e3692 100644 --- a/frontend/src/api/admin/channels.ts +++ b/frontend/src/api/admin/channels.ts @@ -33,6 +33,7 @@ export interface ChannelTimePricingPeriod { export interface ChannelTimePricing { timezone: string + weekdays_only?: boolean periods: ChannelTimePricingPeriod[] } diff --git a/frontend/src/components/admin/channel/TimePricingSection.vue b/frontend/src/components/admin/channel/TimePricingSection.vue index b2e31ca46d..ae329f3680 100644 --- a/frontend/src/components/admin/channel/TimePricingSection.vue +++ b/frontend/src/components/admin/channel/TimePricingSection.vue @@ -1,22 +1,40 @@