feat(model):优化模型价格设置功能
This commit is contained in:
1 parent
add95ca5ab
commit
ee96a5b866
10 files changed
+181
-11
No files matched your search
@@ -128,6 +128,10 @@ type PricingGetReq struct {
|
||||
type PricingGetRes struct {
|
||||
List []PricingItem `json:"list"`
|
||||
TimeSegments []TimeSegmentItem `json:"time_segments" dc:"时段定价列表(锚点行 time_segments)"`
|
||||
// 以下为锚点行展示字段(仅 min_tokens=0 行生效)
|
||||
PriceNote string `json:"price_note" dc:"价格说明(仅管理后台可见的内部备注)"`
|
||||
DiscountLabel string `json:"discount_label" dc:"折扣标签(对外展示,如 7折起)"`
|
||||
PriceChangeNote string `json:"price_change_note" dc:"价格调整说明(对外展示,提示价格有变动)"`
|
||||
}
|
||||
|
||||
// PricingSetReq 设置模型定价(全量替换)
|
||||
@@ -136,6 +140,10 @@ type PricingSetReq struct {
|
||||
ModelID int64 `json:"model_id" in:"path" v:"required" dc:"模型ID"`
|
||||
Items []PricingItem `json:"items" v:"required" dc:"定价列表"`
|
||||
TimeSegments []TimeSegmentItem `json:"time_segments" dc:"时段定价(可选,全量替换;空数组清除时段配置)"`
|
||||
// 以下为锚点行展示字段(全量替换语义:空=清除;price_note 不透出到租户端)
|
||||
PriceNote string `json:"price_note" dc:"价格说明(仅内部可见)"`
|
||||
DiscountLabel string `json:"discount_label" dc:"折扣标签(对外展示)"`
|
||||
PriceChangeNote string `json:"price_change_note" dc:"价格调整说明(对外展示)"`
|
||||
}
|
||||
|
||||
type PricingSetRes struct{}
|
||||
|
||||
@@ -60,6 +60,10 @@ type TenantAvailableModelItem struct {
|
||||
PricingTiers []PricingTierItem `json:"pricing_tiers"`
|
||||
// TimePrices 时段价目(平台配置了时段定价的模型才有;价格为换算后的展示价)
|
||||
TimePrices []TimePriceItem `json:"time_prices"`
|
||||
// DiscountLabel 折扣标签(平台定价锚点行配置的营销展示文案,如"7折起";NULL=不展示)
|
||||
DiscountLabel *string `json:"discount_label"`
|
||||
// PriceChangeNote 价格调整说明(对外提示价格有变动;NULL=不展示)
|
||||
PriceChangeNote *string `json:"price_change_note"`
|
||||
// AsyncImage 图片模型异步端点是否可用(提交 /v1/images/generations/async 后轮询取图)。
|
||||
// 仅图片分类模型有意义;真异步厂商,或同步厂商且「同步图片异步化」开关开启时为 true。
|
||||
AsyncImage bool `json:"async_image"`
|
||||
|
||||
@@ -34,6 +34,9 @@ type MdlPricingColumns struct {
|
||||
CacheReadPrice string // 缓存读取每 1M token 价格(直接定价)
|
||||
CacheCreationPrice string // 缓存创建每 1M token 价格(直接定价)
|
||||
TimeSegments string // 时段定价(JSONB 有序数组,仅 min_tokens=0 锚点行生效):[{"name":"闲时","days":[1,2,3,4,5],"start_time":"00:00","end_time":"08:00","valid_from":"","valid_to":"","multiplier":0.5}],按序先命中先生效,未命中=默认价(乘数 1.0),days 1=周一..7=周日 空=每天,end<start 表示跨零点
|
||||
PriceNote string // 价格说明(仅管理后台可见,调价背景等内部备注),仅 min_tokens=0 锚点行使用,NULL=无
|
||||
DiscountLabel string // 折扣标签(对外展示,如"7折起"、"限时5折"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
|
||||
PriceChangeNote string // 价格调整说明(对外展示,提示用户价格有变动,如"9月1日起输入价下调"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
|
||||
}
|
||||
|
||||
// mdlPricingColumns holds the columns for the table mdl_pricing.
|
||||
@@ -51,6 +54,9 @@ var mdlPricingColumns = MdlPricingColumns{
|
||||
CacheReadPrice: "cache_read_price",
|
||||
CacheCreationPrice: "cache_creation_price",
|
||||
TimeSegments: "time_segments",
|
||||
PriceNote: "price_note",
|
||||
DiscountLabel: "discount_label",
|
||||
PriceChangeNote: "price_change_note",
|
||||
}
|
||||
|
||||
// NewMdlPricingDao creates and returns a new DAO object for table data access.
|
||||
|
||||
@@ -452,6 +452,9 @@ func (s *sAdmin) GetModelPricing(ctx context.Context, req *v1.PricingGetReq) (*v
|
||||
CacheReadPrice float64 `json:"cache_read_price"`
|
||||
CacheCreationPrice float64 `json:"cache_creation_price"`
|
||||
TimeSegments string `json:"time_segments"`
|
||||
PriceNote *string `json:"price_note"`
|
||||
DiscountLabel *string `json:"discount_label"`
|
||||
PriceChangeNote *string `json:"price_change_note"`
|
||||
}
|
||||
|
||||
err := dao.MdlPricing.Ctx(ctx).
|
||||
@@ -464,7 +467,9 @@ func (s *sAdmin) GetModelPricing(ctx context.Context, req *v1.PricingGetReq) (*v
|
||||
|
||||
result := make([]v1.PricingItem, 0, len(rows))
|
||||
var timeSegments []billing.TimeSegment
|
||||
for _, r := range rows {
|
||||
var priceNote, discountLabel, priceChangeNote string
|
||||
for i := range rows {
|
||||
r := rows[i]
|
||||
result = append(result, v1.PricingItem{
|
||||
BillingMode: r.BillingMode,
|
||||
MinTokens: r.MinTokens,
|
||||
@@ -475,15 +480,34 @@ func (s *sAdmin) GetModelPricing(ctx context.Context, req *v1.PricingGetReq) (*v
|
||||
CacheReadPrice: r.CacheReadPrice,
|
||||
CacheCreationPrice: r.CacheCreationPrice,
|
||||
})
|
||||
// 时段定价只认锚点行(min_tokens=0),阶梯行上的残留忽略
|
||||
if r.MinTokens == 0 && r.TimeSegments != "" && r.TimeSegments != "null" {
|
||||
if err := json.Unmarshal([]byte(r.TimeSegments), &timeSegments); err != nil {
|
||||
g.Log().Warningf(ctx, "admin: 模型 %d 时段定价解析失败: %v", req.ModelID, err)
|
||||
timeSegments = nil
|
||||
// 时段定价与展示字段只认锚点行(min_tokens=0),阶梯行上的残留忽略
|
||||
if r.MinTokens == 0 {
|
||||
if r.TimeSegments != "" && r.TimeSegments != "null" {
|
||||
if err := json.Unmarshal([]byte(r.TimeSegments), &timeSegments); err != nil {
|
||||
g.Log().Warningf(ctx, "admin: 模型 %d 时段定价解析失败: %v", req.ModelID, err)
|
||||
timeSegments = nil
|
||||
}
|
||||
}
|
||||
priceNote = derefStr(r.PriceNote)
|
||||
discountLabel = derefStr(r.DiscountLabel)
|
||||
priceChangeNote = derefStr(r.PriceChangeNote)
|
||||
}
|
||||
}
|
||||
return &v1.PricingGetRes{List: result, TimeSegments: timeSegmentsToAPI(timeSegments)}, nil
|
||||
return &v1.PricingGetRes{
|
||||
List: result,
|
||||
TimeSegments: timeSegmentsToAPI(timeSegments),
|
||||
PriceNote: priceNote,
|
||||
DiscountLabel: discountLabel,
|
||||
PriceChangeNote: priceChangeNote,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// derefStr 字符串指针安全解引用(NULL → 空串)
|
||||
func derefStr(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
// timeSegmentsToAPI billing.TimeSegment → API 时段项
|
||||
@@ -556,6 +580,29 @@ func writeTimeSegmentsForModel(ctx context.Context, modelDBID int64, items []v1.
|
||||
return err
|
||||
}
|
||||
|
||||
// writePricingDisplayFieldsForModel 把展示字段(价格说明/折扣标签/价格调整说明)写到定价锚点行(min_tokens=0)。
|
||||
// 全量替换语义:空串=清除(写 NULL)。与 writeTimeSegmentsForModel 同理用列名直写而非 do 字段,
|
||||
// 与 gf gen dao 生成物解耦。调用方需保证事务 ctx 传播。
|
||||
func writePricingDisplayFieldsForModel(ctx context.Context, modelDBID int64, priceNote, discountLabel, priceChangeNote string) error {
|
||||
trimOrNull := func(s string) any {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
_, err := dao.MdlPricing.Ctx(ctx).
|
||||
Where("model_id", modelDBID).
|
||||
Where("min_tokens", 0).
|
||||
Data(g.Map{
|
||||
"price_note": trimOrNull(priceNote),
|
||||
"discount_label": trimOrNull(discountLabel),
|
||||
"price_change_note": trimOrNull(priceChangeNote),
|
||||
}).
|
||||
Update()
|
||||
return err
|
||||
}
|
||||
|
||||
// SetModelPricing 设置模型定价(全量替换)
|
||||
func (s *sAdmin) SetModelPricing(ctx context.Context, req *v1.PricingSetReq) (*v1.PricingSetRes, error) {
|
||||
// 时段定价校验(乘数范围/时间格式/星期/日期边界),fail-fast 返回用户可读错误
|
||||
@@ -623,6 +670,11 @@ func (s *sAdmin) SetModelPricing(ctx context.Context, req *v1.PricingSetReq) (*v
|
||||
return err
|
||||
}
|
||||
|
||||
// 展示字段写锚点行(空=清除,全量替换语义;需在锚点行插入之后执行)
|
||||
if err := writePricingDisplayFieldsForModel(ctx, req.ModelID, req.PriceNote, req.DiscountLabel, req.PriceChangeNote); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -37,6 +37,8 @@ type tenantModelPriceRow struct {
|
||||
BaseCacheCreationPrice float64 `json:"base_cache_creation_price"`
|
||||
BaseBillingMode string `json:"base_billing_mode"`
|
||||
BasePerRequestPrice *float64 `json:"base_per_request_price"`
|
||||
BaseDiscountLabel *string `json:"base_discount_label"`
|
||||
BasePriceChangeNote *string `json:"base_price_change_note"`
|
||||
CustomInputPrice *float64 `json:"custom_input_price"`
|
||||
CustomOutputPrice *float64 `json:"custom_output_price"`
|
||||
CustomCacheReadPrice *float64 `json:"custom_cache_read_price"`
|
||||
@@ -53,6 +55,8 @@ type groupPriceRow struct {
|
||||
BaseCacheReadPrice float64 `json:"base_cache_read_price"`
|
||||
BaseCacheCreationPrice float64 `json:"base_cache_creation_price"`
|
||||
BasePerRequestPrice *float64 `json:"base_per_request_price"`
|
||||
BaseDiscountLabel *string `json:"base_discount_label"`
|
||||
BasePriceChangeNote *string `json:"base_price_change_note"`
|
||||
}
|
||||
|
||||
// baseTierRow 阶梯定价查询结果
|
||||
@@ -83,6 +87,8 @@ type priceInfo struct {
|
||||
BaseCacheCreationPrice float64
|
||||
BaseBillingMode string
|
||||
BasePerRequestPrice *float64
|
||||
BaseDiscountLabel *string
|
||||
BasePriceChangeNote *string
|
||||
CustomInputPrice *float64
|
||||
CustomOutputPrice *float64
|
||||
CustomCacheReadPrice *float64
|
||||
@@ -98,6 +104,8 @@ type groupPriceInfo struct {
|
||||
BaseCacheReadPrice float64
|
||||
BaseCacheCreationPrice float64
|
||||
BasePerRequestPrice *float64
|
||||
BaseDiscountLabel *string
|
||||
BasePriceChangeNote *string
|
||||
}
|
||||
|
||||
// ListAvailableModels 获取租户可用的模型列表
|
||||
@@ -128,7 +136,7 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
LeftJoin("mdl_pricing p ON p.model_id = mdl_tenant_models.model_id AND p.min_tokens = 0").
|
||||
Where("mdl_tenant_models.tenant_id", tenantID).
|
||||
WhereIn("mdl_tenant_models.model_id", explicitDBIDs).
|
||||
Fields("mdl_tenant_models.model_id AS model_db_id, mdl_tenant_models.id, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, p.input_price AS base_input_price, p.output_price AS base_output_price, p.cache_read_price AS base_cache_read_price, p.cache_creation_price AS base_cache_creation_price, p.billing_mode AS base_billing_mode, p.per_request_price AS base_per_request_price, mdl_tenant_models.custom_input_price, mdl_tenant_models.custom_output_price, mdl_tenant_models.custom_cache_read_price, mdl_tenant_models.custom_cache_creation_price, mdl_tenant_models.custom_pricing_tiers").
|
||||
Fields("mdl_tenant_models.model_id AS model_db_id, mdl_tenant_models.id, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, p.input_price AS base_input_price, p.output_price AS base_output_price, p.cache_read_price AS base_cache_read_price, p.cache_creation_price AS base_cache_creation_price, p.billing_mode AS base_billing_mode, p.per_request_price AS base_per_request_price, p.discount_label AS base_discount_label, p.price_change_note AS base_price_change_note, mdl_tenant_models.custom_input_price, mdl_tenant_models.custom_output_price, mdl_tenant_models.custom_cache_read_price, mdl_tenant_models.custom_cache_creation_price, mdl_tenant_models.custom_pricing_tiers").
|
||||
Scan(&priceResults)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -150,6 +158,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
BaseCacheCreationPrice: r.BaseCacheCreationPrice,
|
||||
BaseBillingMode: r.BaseBillingMode,
|
||||
BasePerRequestPrice: r.BasePerRequestPrice,
|
||||
BaseDiscountLabel: r.BaseDiscountLabel,
|
||||
BasePriceChangeNote: r.BasePriceChangeNote,
|
||||
CustomInputPrice: r.CustomInputPrice,
|
||||
CustomOutputPrice: r.CustomOutputPrice,
|
||||
CustomCacheReadPrice: r.CustomCacheReadPrice,
|
||||
@@ -172,7 +182,7 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
err = dao.MdlPricing.Ctx(ctx).
|
||||
WhereIn("model_id", groupDBIDs).
|
||||
Where("min_tokens", 0).
|
||||
Fields("model_id, billing_mode AS base_billing_mode, input_price AS base_input_price, output_price AS base_output_price, cache_read_price AS base_cache_read_price, cache_creation_price AS base_cache_creation_price, per_request_price AS base_per_request_price").
|
||||
Fields("model_id, billing_mode AS base_billing_mode, input_price AS base_input_price, output_price AS base_output_price, cache_read_price AS base_cache_read_price, cache_creation_price AS base_cache_creation_price, per_request_price AS base_per_request_price, discount_label AS base_discount_label, price_change_note AS base_price_change_note").
|
||||
Scan(&groupPrices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -186,6 +196,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
BaseCacheReadPrice: gp.BaseCacheReadPrice,
|
||||
BaseCacheCreationPrice: gp.BaseCacheCreationPrice,
|
||||
BasePerRequestPrice: gp.BasePerRequestPrice,
|
||||
BaseDiscountLabel: gp.BaseDiscountLabel,
|
||||
BasePriceChangeNote: gp.BasePriceChangeNote,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,6 +313,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
OutputPrice: outputPrice,
|
||||
CacheReadPrice: cacheReadPrice,
|
||||
CacheCreationPrice: cacheCreationPrice,
|
||||
DiscountLabel: pi.BaseDiscountLabel,
|
||||
PriceChangeNote: pi.BasePriceChangeNote,
|
||||
}
|
||||
|
||||
if effectiveBillingMode == "tiered" {
|
||||
@@ -316,6 +330,7 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
billingMode := "token"
|
||||
var inputPrice, outputPrice, cacheReadPrice, cacheCreationPrice, perRequestPrice *float64
|
||||
var baseInputPrice, baseOutputPrice float64
|
||||
var discountLabel, priceChangeNote *string
|
||||
gp, ok := groupPriceMap[m.ModelDBID]
|
||||
if ok {
|
||||
billingMode = gp.BaseBillingMode
|
||||
@@ -329,6 +344,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
cacheReadPrice = effectivePrice(nil, gp.BaseCacheReadPrice)
|
||||
cacheCreationPrice = effectivePrice(nil, gp.BaseCacheCreationPrice)
|
||||
perRequestPrice = gp.BasePerRequestPrice
|
||||
discountLabel = gp.BaseDiscountLabel
|
||||
priceChangeNote = gp.BasePriceChangeNote
|
||||
}
|
||||
|
||||
item := v1.TenantAvailableModelItem{
|
||||
@@ -347,6 +364,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
OutputPrice: outputPrice,
|
||||
CacheReadPrice: cacheReadPrice,
|
||||
CacheCreationPrice: cacheCreationPrice,
|
||||
DiscountLabel: discountLabel,
|
||||
PriceChangeNote: priceChangeNote,
|
||||
}
|
||||
if billingMode == "tiered" && ok {
|
||||
item.PricingTiers = buildTiers("", baseInputPrice, baseOutputPrice, baseTiersMap[m.ModelDBID])
|
||||
|
||||
@@ -26,4 +26,7 @@ type MdlPricing struct {
|
||||
CacheReadPrice any // 缓存读取每 1M token 价格(直接定价)
|
||||
CacheCreationPrice any // 缓存创建每 1M token 价格(直接定价)
|
||||
TimeSegments any // 时段定价(JSONB 有序数组,仅 min_tokens=0 锚点行生效):[{"name":"闲时","days":[1,2,3,4,5],"start_time":"00:00","end_time":"08:00","valid_from":"","valid_to":"","multiplier":0.5}],按序先命中先生效,未命中=默认价(乘数 1.0),days 1=周一..7=周日 空=每天,end<start 表示跨零点
|
||||
PriceNote any // 价格说明(仅管理后台可见,调价背景等内部备注),仅 min_tokens=0 锚点行使用,NULL=无
|
||||
DiscountLabel any // 折扣标签(对外展示,如"7折起"、"限时5折"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
|
||||
PriceChangeNote any // 价格调整说明(对外展示,提示用户价格有变动,如"9月1日起输入价下调"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
|
||||
}
|
||||
@@ -24,4 +24,7 @@ type MdlPricing struct {
|
||||
CacheReadPrice decimal.Decimal `json:"cache_read_price" orm:"cache_read_price" description:"缓存读取每 1M token 价格(直接定价)"` // 缓存读取每 1M token 价格(直接定价)
|
||||
CacheCreationPrice decimal.Decimal `json:"cache_creation_price" orm:"cache_creation_price" description:"缓存创建每 1M token 价格(直接定价)"` // 缓存创建每 1M token 价格(直接定价)
|
||||
TimeSegments string `json:"time_segments" orm:"time_segments" description:"时段定价(JSONB 有序数组,仅 min_tokens=0 锚点行生效):[{\"name\":\"闲时\",\"days\":[1,2,3,4,5],\"start_time\":\"00:00\",\"end_time\":\"08:00\",\"valid_from\":\"\",\"valid_to\":\"\",\"multiplier\":0.5}],按序先命中先生效,未命中=默认价(乘数 1.0),days 1=周一..7=周日 空=每天,end<start 表示跨零点"` // 时段定价(JSONB 有序数组,仅 min_tokens=0 锚点行生效):[{"name":"闲时","days":[1,2,3,4,5],"start_time":"00:00","end_time":"08:00","valid_from":"","valid_to":"","multiplier":0.5}],按序先命中先生效,未命中=默认价(乘数 1.0),days 1=周一..7=周日 空=每天,end<start 表示跨零点
|
||||
PriceNote string `json:"price_note" orm:"price_note" description:"价格说明(仅管理后台可见,调价背景等内部备注),仅 min_tokens=0 锚点行使用,NULL=无"` // 价格说明(仅管理后台可见,调价背景等内部备注),仅 min_tokens=0 锚点行使用,NULL=无
|
||||
DiscountLabel string `json:"discount_label" orm:"discount_label" description:"折扣标签(对外展示,如\"7折起\"、\"限时5折\"),仅 min_tokens=0 锚点行使用,NULL/空=不展示"` // 折扣标签(对外展示,如"7折起"、"限时5折"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
|
||||
PriceChangeNote string `json:"price_change_note" orm:"price_change_note" description:"价格调整说明(对外展示,提示用户价格有变动,如\"9月1日起输入价下调\"),仅 min_tokens=0 锚点行使用,NULL/空=不展示"` // 价格调整说明(对外展示,提示用户价格有变动,如"9月1日起输入价下调"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
-- +goose Up
|
||||
-- 模型定价展示字段:价格说明(内部)+ 折扣标签 / 价格调整说明(对外展示)
|
||||
ALTER TABLE mdl_pricing
|
||||
ADD COLUMN IF NOT EXISTS price_note VARCHAR(500),
|
||||
ADD COLUMN IF NOT EXISTS discount_label VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS price_change_note VARCHAR(200);
|
||||
|
||||
COMMENT ON COLUMN mdl_pricing.price_note IS '价格说明(仅管理后台可见,调价背景等内部备注),仅 min_tokens=0 锚点行使用,NULL=无';
|
||||
COMMENT ON COLUMN mdl_pricing.discount_label IS '折扣标签(对外展示,如"7折起"、"限时5折"),仅 min_tokens=0 锚点行使用,NULL/空=不展示';
|
||||
COMMENT ON COLUMN mdl_pricing.price_change_note IS '价格调整说明(对外展示,提示用户价格有变动,如"9月1日起输入价下调"),仅 min_tokens=0 锚点行使用,NULL/空=不展示';
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE mdl_pricing
|
||||
DROP COLUMN IF EXISTS price_change_note,
|
||||
DROP COLUMN IF EXISTS discount_label,
|
||||
DROP COLUMN IF EXISTS price_note;
|
||||
@@ -23,6 +23,11 @@ const editorSaving = ref(false)
|
||||
const editorBillingMode = ref('token')
|
||||
const editorItems = reactive<any[]>([])
|
||||
|
||||
// 展示字段(挂锚点行:内部备注 + 对外营销文案)
|
||||
const editorPriceNote = ref('')
|
||||
const editorDiscountLabel = ref('')
|
||||
const editorPriceChangeNote = ref('')
|
||||
|
||||
const billingModeOptions = [
|
||||
{ label: '按量计费 (Token)', value: 'token' },
|
||||
{ label: '按次计费', value: 'per_request' },
|
||||
@@ -46,6 +51,9 @@ function resetEditorDefaults() {
|
||||
editorBillingMode.value = 'token'
|
||||
editorItems.length = 0
|
||||
editorItems.push(createEmptyItem('token'))
|
||||
editorPriceNote.value = ''
|
||||
editorDiscountLabel.value = ''
|
||||
editorPriceChangeNote.value = ''
|
||||
resetTimeSegments()
|
||||
}
|
||||
|
||||
@@ -124,6 +132,9 @@ async function loadPricing() {
|
||||
try {
|
||||
const res: any = await request.get(`/admin/models/${props.modelId}/pricing`)
|
||||
const list: any[] = res.data?.data?.list || []
|
||||
editorPriceNote.value = res.data?.data?.price_note || ''
|
||||
editorDiscountLabel.value = res.data?.data?.discount_label || ''
|
||||
editorPriceChangeNote.value = res.data?.data?.price_change_note || ''
|
||||
if (list.length > 0) {
|
||||
editorBillingMode.value = list[0].billing_mode || 'token'
|
||||
editorItems.length = 0
|
||||
@@ -379,7 +390,13 @@ async function savePricing() {
|
||||
cache_read_price: item.cache_read_price,
|
||||
cache_creation_price: item.cache_creation_price,
|
||||
}))
|
||||
await request.put(`/admin/models/${props.modelId}/pricing`, { items, time_segments: segmentsPayload() })
|
||||
await request.put(`/admin/models/${props.modelId}/pricing`, {
|
||||
items,
|
||||
time_segments: segmentsPayload(),
|
||||
price_note: editorPriceNote.value.trim(),
|
||||
discount_label: editorDiscountLabel.value.trim(),
|
||||
price_change_note: editorPriceChangeNote.value.trim(),
|
||||
})
|
||||
Message.success('定价已保存')
|
||||
emit('update:visible', false)
|
||||
emit('saved')
|
||||
@@ -748,6 +765,30 @@ watch(() => props.visible, (val) => {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 展示信息(折扣标签 / 价格调整说明对外展示,价格说明仅内部可见) -->
|
||||
<div class="editor-section">
|
||||
<div class="editor-section-header">
|
||||
<h3>展示信息</h3>
|
||||
<span class="section-hint">折扣标签与调整说明会展示给租户;价格说明仅管理后台可见</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4">
|
||||
<AFormItem label="折扣标签(对外展示)">
|
||||
<AInput v-model="editorDiscountLabel" :maxlength="50" placeholder="如:7折起、限时5折" allow-clear />
|
||||
</AFormItem>
|
||||
<AFormItem label="价格调整说明(对外展示)">
|
||||
<AInput v-model="editorPriceChangeNote" :maxlength="200" placeholder="如:9月1日起输入价下调 20%" allow-clear />
|
||||
</AFormItem>
|
||||
</div>
|
||||
<AFormItem label="价格说明(仅内部可见)">
|
||||
<ATextarea
|
||||
v-model="editorPriceNote"
|
||||
:max-length="500"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
placeholder="调价背景、渠道成本等内部备注,不对外展示"
|
||||
/>
|
||||
</AFormItem>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</AForm>
|
||||
</ASpin>
|
||||
|
||||
@@ -51,6 +51,8 @@ interface ModelItem {
|
||||
cache_creation_price: number | null
|
||||
pricing_tiers: PricingTierItem[]
|
||||
time_prices?: TimePriceItem[] | null
|
||||
discount_label?: string | null
|
||||
price_change_note?: string | null
|
||||
}
|
||||
|
||||
const capabilityLabel: Record<string, string> = {
|
||||
@@ -217,7 +219,7 @@ function timeWindow(tp: TimePriceItem): string {
|
||||
|
||||
function timePriceText(m: ModelItem, tp: TimePriceItem): string {
|
||||
if (m.billing_mode === 'per_request') return `${formatPrice(tp.per_request_price ?? null)} /次`
|
||||
if (m.billing_mode === 'tiered') return `${formatPrice(tp.input_price ?? null)} 起`
|
||||
if (m.billing_mode === 'tiered') return `输入 ${formatPrice(tp.input_price ?? null)} · 输出 ${formatPrice(tp.output_price ?? null)} 起`
|
||||
return `输入 ${formatPrice(tp.input_price ?? null)} · 输出 ${formatPrice(tp.output_price ?? null)}`
|
||||
}
|
||||
|
||||
@@ -334,6 +336,13 @@ onMounted(fetchModels)
|
||||
{{ categoryLabel[m.category] || m.category }}
|
||||
</span>
|
||||
<h3 class="text-sm font-semibold text-gray-900 truncate">{{ m.model_name || m.model_id }}</h3>
|
||||
<span
|
||||
v-if="m.discount_label"
|
||||
class="shrink-0 px-1.5 py-0.5 text-[10px] font-semibold text-white rounded-md"
|
||||
style="background: linear-gradient(135deg, #f43f5e, #fb923c);"
|
||||
>
|
||||
{{ m.discount_label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Model ID + Copy -->
|
||||
@@ -382,6 +391,15 @@ onMounted(fetchModels)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 价格调整说明(平台调价提示,仅配置时展示) -->
|
||||
<div
|
||||
v-if="m.price_change_note"
|
||||
class="mt-2.5 flex items-start gap-1.5 text-[11px] text-amber-700 bg-amber-50 border border-amber-100 rounded-lg px-2 py-1.5"
|
||||
>
|
||||
<Icon name="infoCircle" size="xs" class="shrink-0 mt-px text-amber-500" />
|
||||
<span>{{ m.price_change_note }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Pricing (pushed to bottom) -->
|
||||
<div v-if="hasPricing(m)" class="mt-auto pt-3 border-t border-gray-100">
|
||||
<template v-if="m.billing_mode === 'token'">
|
||||
|
||||
Reference in new issue
Block a user