diff --git a/backend/internal/handler/model_plaza_handler.go b/backend/internal/handler/model_plaza_handler.go
index 551b8cc99..829ef94af 100644
--- a/backend/internal/handler/model_plaza_handler.go
+++ b/backend/internal/handler/model_plaza_handler.go
@@ -54,9 +54,11 @@ type modelPlazaTimePricingPeriod struct {
}
// modelPlazaTimePricing 计费会生效的分时倍率(仅倍率 ≠ 1 的时段)。
+// WeekdaysOnly 为 true 时时段仅周一至周五生效,周末整天按标准价计费。
type modelPlazaTimePricing struct {
- Timezone string `json:"timezone"`
- Periods []modelPlazaTimePricingPeriod `json:"periods"`
+ Timezone string `json:"timezone"`
+ WeekdaysOnly bool `json:"weekdays_only,omitempty"`
+ Periods []modelPlazaTimePricingPeriod `json:"periods"`
}
// modelPlazaModel 广场模型条目:实收口径展示定价(白名单形态)+ 官方参考价。
@@ -226,7 +228,7 @@ func toModelPlazaTimePricing(p *service.TimePricingSchedule) *modelPlazaTimePric
Multiplier: period.Multiplier,
})
}
- return &modelPlazaTimePricing{Timezone: p.Timezone, Periods: periods}
+ return &modelPlazaTimePricing{Timezone: p.Timezone, WeekdaysOnly: p.WeekdaysOnly, Periods: periods}
}
// toModelPlazaOfficialPricing 转换官方参考价;nil 透传(前端显示 "-")。
diff --git a/backend/internal/handler/model_plaza_handler_test.go b/backend/internal/handler/model_plaza_handler_test.go
index 724ca1903..ab37a6db2 100644
--- a/backend/internal/handler/model_plaza_handler_test.go
+++ b/backend/internal/handler/model_plaza_handler_test.go
@@ -192,6 +192,13 @@ func TestToModelPlazaGroupDTO_TimePricing(t *testing.T) {
TimePricing: &service.TimePricingSchedule{Timezone: "Asia/Shanghai", Periods: []service.TimePricingPeriod{
{StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
}},
+ }, {
+ Name: "deepseek-reasoner",
+ Platform: "deepseek",
+ Pricing: &service.ChannelModelPricing{BillingMode: service.BillingModeToken, InputPrice: testPtr(0.56e-6)},
+ TimePricing: &service.TimePricingSchedule{Timezone: "Asia/Shanghai", WeekdaysOnly: true, Periods: []service.TimePricingPeriod{
+ {StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
+ }},
}},
}
raw, err := json.Marshal(toModelPlazaGroupDTO(&g, nil))
@@ -201,10 +208,16 @@ func TestToModelPlazaGroupDTO_TimePricing(t *testing.T) {
model := decoded["models"].([]any)[0].(map[string]any)
tp := model["time_pricing"].(map[string]any)
require.Equal(t, "Asia/Shanghai", tp["timezone"])
+ _, hasWeekdaysOnly := tp["weekdays_only"]
+ require.False(t, hasWeekdaysOnly, "未开启仅工作日时字段省略")
periods := tp["periods"].([]any)
require.Len(t, periods, 1)
first := periods[0].(map[string]any)
require.Equal(t, "00:30", first["start_time"])
require.Equal(t, "08:30", first["end_time"])
require.InDelta(t, 0.5, first["multiplier"].(float64), 1e-12)
+
+ weekdaysModel := decoded["models"].([]any)[1].(map[string]any)
+ weekdaysTP := weekdaysModel["time_pricing"].(map[string]any)
+ require.Equal(t, true, weekdaysTP["weekdays_only"])
}
diff --git a/backend/internal/service/billing_context_schedule.go b/backend/internal/service/billing_context_schedule.go
index 8a43283f2..05f2a181b 100644
--- a/backend/internal/service/billing_context_schedule.go
+++ b/backend/internal/service/billing_context_schedule.go
@@ -39,9 +39,11 @@ type TimePricingPeriod struct {
}
// TimePricingSchedule 分组+模型生效的分时倍率(仅列出倍率 ≠ 1 的时段,按开始时间升序)。
+// WeekdaysOnly 为 true 时时段仅周一至周五生效,周末整天按标准价计费。
type TimePricingSchedule struct {
- Timezone string
- Periods []TimePricingPeriod
+ Timezone string
+ WeekdaysOnly bool
+ Periods []TimePricingPeriod
}
// ContextPricingSchedule 分组+模型按上下文长度分档的有效单价表。
@@ -168,8 +170,10 @@ func resolvedTimePricingSchedule(resolved *ResolvedPricing) *TimePricingSchedule
if err != nil {
continue
}
- // 时段按每日循环,取任意一天该时段开始后 1 秒作为探针时刻。
- at := time.Date(2026, time.January, 1, 0, 0, start+1, 0, location)
+ // 时段按每日循环,取该时段开始后 1 秒作为探针时刻。
+ // 锚点日必须是工作日(2026-01-05 为周一):weekdays_only 配置在周末恒为 1,
+ // 锚点落在周末会把时段整组剔除。
+ at := time.Date(2026, time.January, 5, 0, 0, start+1, 0, location)
multiplier := resolvedChannelTimeMultiplier(resolved, at)
if multiplier == 1 {
continue
@@ -184,7 +188,11 @@ func resolvedTimePricingSchedule(resolved *ResolvedPricing) *TimePricingSchedule
return nil
}
sort.SliceStable(probed, func(i, j int) bool { return probed[i].start < probed[j].start })
- out := &TimePricingSchedule{Timezone: cfg.Timezone, Periods: make([]TimePricingPeriod, 0, len(probed))}
+ out := &TimePricingSchedule{
+ Timezone: cfg.Timezone,
+ WeekdaysOnly: cfg.WeekdaysOnly,
+ Periods: make([]TimePricingPeriod, 0, len(probed)),
+ }
for _, p := range probed {
out.Periods = append(out.Periods, p.period)
}
diff --git a/backend/internal/service/billing_context_schedule_test.go b/backend/internal/service/billing_context_schedule_test.go
index 68ba3c855..b13606da9 100644
--- a/backend/internal/service/billing_context_schedule_test.go
+++ b/backend/internal/service/billing_context_schedule_test.go
@@ -515,6 +515,7 @@ func TestResolveContextPricingSchedule_TimePricing(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, sched.TimePricing)
require.Equal(t, "Asia/Shanghai", sched.TimePricing.Timezone)
+ require.False(t, sched.TimePricing.WeekdaysOnly)
require.Equal(t, []TimePricingPeriod{
{StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
{StartTime: "18:00", EndTime: "22:00:00", Multiplier: 1.2},
@@ -546,6 +547,43 @@ func TestResolveContextPricingSchedule_TimePricing(t *testing.T) {
}
})
+ t.Run("仅工作日配置透传标注且时段仍列出", func(t *testing.T) {
+ weekdays := &ChannelTimePricing{Timezone: "Asia/Shanghai", WeekdaysOnly: true, Periods: []ChannelTimePricingPeriod{
+ {StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
+ }}
+ bs, resolver := newTokenCostTestEnv(t, PlatformAnthropic, sonnetChannelWithTimePricing(weekdays), nil)
+ sched, err := bs.ResolveContextPricingSchedule(context.Background(), resolver, ContextPricingScheduleInput{
+ Model: "claude-sonnet-4", Group: enabledGroup(PlatformAnthropic), Platform: PlatformAnthropic,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, sched.TimePricing, "探针锚点必须落在工作日,否则仅工作日配置的时段会被整组剔除")
+ require.True(t, sched.TimePricing.WeekdaysOnly)
+ require.Equal(t, []TimePricingPeriod{
+ {StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
+ }, sched.TimePricing.Periods)
+
+ // 对账:工作日时段内乘倍率,周末同一时段按标准价
+ group := enabledGroup(PlatformAnthropic)
+ gid := group.ID
+ resolved := resolver.Resolve(context.Background(), PricingInput{Model: "claude-sonnet-4", GroupID: &gid, Group: group})
+ loc, err := time.LoadLocation("Asia/Shanghai")
+ require.NoError(t, err)
+ for _, tc := range []struct {
+ at time.Time
+ want float64
+ }{
+ {time.Date(2026, 8, 24, 3, 0, 0, 0, loc), 0.5}, // 周一
+ {time.Date(2026, 8, 23, 3, 0, 0, 0, loc), 1}, // 周日
+ } {
+ cost, err := bs.CalculateTokenCostForRequest(TokenCostRequest{
+ Ctx: context.Background(), Model: "claude-sonnet-4", Group: group, Tokens: UsageTokens{InputTokens: 1000},
+ RateMultiplier: 1, PricingAt: tc.at, Resolver: resolver, Resolved: resolved,
+ })
+ require.NoError(t, err)
+ assertCostClose(t, 1000*2e-6*tc.want, cost.ActualCost, "at %s", tc.at)
+ }
+ })
+
t.Run("配置非法时计费按 1 计,阶梯表不列分时", func(t *testing.T) {
invalid := &ChannelTimePricing{Timezone: "Asia/Shanghai", Periods: []ChannelTimePricingPeriod{
{StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
diff --git a/frontend/src/api/modelPlaza.ts b/frontend/src/api/modelPlaza.ts
index 2c13ed113..e96a62193 100644
--- a/frontend/src/api/modelPlaza.ts
+++ b/frontend/src/api/modelPlaza.ts
@@ -38,6 +38,8 @@ export interface PlazaTimePricingPeriod {
export interface PlazaTimePricing {
/** IANA 时区名,如 Asia/Shanghai。 */
timezone: string
+ /** true 时时段仅周一至周五生效,周末整天按标准价计费。 */
+ weekdays_only?: boolean
periods: PlazaTimePricingPeriod[]
}
diff --git a/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue b/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue
index 86a5a41b0..7a5154910 100644
--- a/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue
+++ b/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue
@@ -70,8 +70,11 @@
+ {{
+ t('modelPlaza.table.timePricingWeekdays')
+ }}
{{ formatTimeWindow(period) }}
v.replace(/^(\d{2}:\d{2}):00$/, '$1')
diff --git a/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts b/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts
index 180789ae4..78a5c0c76 100644
--- a/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts
+++ b/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts
@@ -582,6 +582,26 @@ describe('PlazaModelPricingTable 分时计价', () => {
expect(nightCells[4].text()).toContain('$3.00')
})
+ it('仅工作日生效时时段行带工作日前缀,tooltip 换用周末回落文案', () => {
+ const model = timePricedModel()
+ model.time_pricing!.weekdays_only = true
+ const wrapper = mountTable([model], 1)
+ const trs = wrapper.findAll('tbody tr')
+ expect(trs).toHaveLength(3)
+
+ const nightCells = trs[1].findAll('td')
+ expect(nightCells[0].text()).toContain('modelPlaza.table.timePricingWeekdays')
+ expect(nightCells[0].text()).toContain('00:30–08:30')
+ expect(nightCells[0].find('[title="modelPlaza.table.timePricingRowHintWeekdays"]').exists()).toBe(true)
+ expect(nightCells[0].find('[title="modelPlaza.table.timePricingRowHint"]').exists()).toBe(false)
+ })
+
+ it('每日生效(无 weekdays_only)不渲染工作日前缀', () => {
+ const wrapper = mountTable([timePricedModel()], 1)
+ expect(wrapper.find('tbody').text()).not.toContain('modelPlaza.table.timePricingWeekdays')
+ expect(wrapper.find('[title="modelPlaza.table.timePricingRowHint"]').exists()).toBe(true)
+ })
+
it('无分时倍率时只有一行,不渲染时段标注', () => {
const wrapper = mountTable([tokenModel()], 1)
expect(wrapper.findAll('tbody tr')).toHaveLength(1)
diff --git a/frontend/src/i18n/locales/en/dashboard.ts b/frontend/src/i18n/locales/en/dashboard.ts
index 2bfe635cf..6e9a901cc 100644
--- a/frontend/src/i18n/locales/en/dashboard.ts
+++ b/frontend/src/i18n/locales/en/dashboard.ts
@@ -605,6 +605,9 @@ export default {
tierHintMarginal: 'Only the portion above the threshold is billed at this tier; output is unaffected',
marginalBadge: 'excess-only tiers',
timePricingRowHint: 'Requests made within this period ({timezone} time) are billed at the prices in this row',
+ timePricingRowHintWeekdays:
+ 'On weekdays (Mon–Fri) only, requests made within this period ({timezone} time) are billed at the prices in this row; weekends use the standard prices',
+ timePricingWeekdays: 'Weekdays',
timePricingRateHint: 'Effective rate {rate} × period multiplier {multiplier}',
paidPrice: 'Your Price (Discounted)',
officialPrice: 'Official Price',
diff --git a/frontend/src/i18n/locales/zh/dashboard.ts b/frontend/src/i18n/locales/zh/dashboard.ts
index ac8266ca2..c741aa60f 100644
--- a/frontend/src/i18n/locales/zh/dashboard.ts
+++ b/frontend/src/i18n/locales/zh/dashboard.ts
@@ -610,6 +610,9 @@ export default {
tierHintMarginal: '仅超过阈值的部分按该档计价,输出不加价',
marginalBadge: '超出部分计价',
timePricingRowHint: '按 {timezone} 时间,在该时段内发起的请求按本行价格计费',
+ timePricingRowHintWeekdays:
+ '按 {timezone} 时间,仅工作日(周一至周五)在该时段内发起的请求按本行价格计费,周末全天按标准价',
+ timePricingWeekdays: '工作日',
timePricingRateHint: '生效倍率 {rate} × 时段倍率 {multiplier}',
paidPrice: '实付价格(折后)',
officialPrice: '官方价格',