Version: 0.9.81.dev.260506
后端: 1. Credit 价格规则补齐利润率与实际计费单价语义:新增 `profit_rate_bps` 与 `charge_*_price_micros` 展示字段,下沉共享价格推导 helper,tokenstore rpc/client/proto/model/default rule 全链路同步,LLM usage 扣费统一改按加价后的 charge 单价换算。 2. task-class 更新链路修正全量覆盖与归属校验:`runtime/conv` 保留 item id,DAO 更新前显式校验 task-class 与 item 归属,改用显式字段 map 落库 nil/空切片/零值,避免 `RowsAffected=0` 误判越权,同时补齐任务项可编辑字段更新。 3. GormCache task-class 失效补空 user_id 保护:更新语句缺少模型上下文时直接跳过失效,避免缓存插件因空指针影响主事务。 前端: 4. 课表中心补齐任务类编辑能力:新增 `updateTaskClass` API,创建弹窗支持编辑态回填与 item id 提交,日程页支持先拉详情再编辑并在保存后刷新任务类详情与列表。 5. 计划广场详情补点赞交互与奖励提示:详情页新增点赞/取消点赞按钮、奖励反馈文案与计数展示,论坛类型补 `reward_hint`,评论区与帖子作者头像统一接入兜底头像工具。 6. 品牌与展示细节收口:侧边栏与 favicon 切到项目 logo,首页标题改为 `SmartMate`,主面板缩放上限微调,论坛列表头像显示与整体品牌观感同步统一。
This commit is contained in:
@@ -319,18 +319,23 @@ func creditPriceRuleFromPB(item *pb.CreditPriceRuleView) creditcontracts.CreditP
|
||||
return creditcontracts.CreditPriceRuleView{}
|
||||
}
|
||||
return creditcontracts.CreditPriceRuleView{
|
||||
RuleID: item.RuleId,
|
||||
Scene: item.Scene,
|
||||
ProviderName: item.ProviderName,
|
||||
ModelName: item.ModelName,
|
||||
InputPriceMicros: item.InputPriceMicros,
|
||||
OutputPriceMicros: item.OutputPriceMicros,
|
||||
CachedPriceMicros: item.CachedPriceMicros,
|
||||
ReasoningPriceMicros: item.ReasoningPriceMicros,
|
||||
CreditPerYuan: item.CreditPerYuan,
|
||||
Status: item.Status,
|
||||
Priority: int(item.Priority),
|
||||
Description: item.Description,
|
||||
RuleID: item.RuleId,
|
||||
Scene: item.Scene,
|
||||
ProviderName: item.ProviderName,
|
||||
ModelName: item.ModelName,
|
||||
InputPriceMicros: item.InputPriceMicros,
|
||||
OutputPriceMicros: item.OutputPriceMicros,
|
||||
CachedPriceMicros: item.CachedPriceMicros,
|
||||
ReasoningPriceMicros: item.ReasoningPriceMicros,
|
||||
CreditPerYuan: item.CreditPerYuan,
|
||||
ProfitRateBps: item.ProfitRateBps,
|
||||
ChargeInputPriceMicros: item.ChargeInputPriceMicros,
|
||||
ChargeOutputPriceMicros: item.ChargeOutputPriceMicros,
|
||||
ChargeCachedPriceMicros: item.ChargeCachedPriceMicros,
|
||||
ChargeReasoningPriceMicros: item.ChargeReasoningPriceMicros,
|
||||
Status: item.Status,
|
||||
Priority: int(item.Priority),
|
||||
Description: item.Description,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ type CreditPriceRule struct {
|
||||
CachedPriceMicros int64 `gorm:"column:cached_price_micros"`
|
||||
ReasoningPriceMicros int64 `gorm:"column:reasoning_price_micros"`
|
||||
CreditPerYuan int64 `gorm:"column:credit_per_yuan"`
|
||||
ProfitRateBps int64 `gorm:"column:profit_rate_bps"`
|
||||
Status string `gorm:"column:status"`
|
||||
Priority int `gorm:"column:priority"`
|
||||
Description string `gorm:"column:description"`
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
llmdao "github.com/LoveLosita/smartflow/backend/services/llm/dao"
|
||||
creditcontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/creditstore"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -152,19 +153,18 @@ func quoteUsagePrice(rule llmdao.CreditPriceRule, input UsagePricingInput) Usage
|
||||
nonCachedInputTokens := inputTokens - cachedTokens
|
||||
nonReasoningOutputTokens := outputTokens - reasoningTokens
|
||||
|
||||
cachedPriceMicros := rule.CachedPriceMicros
|
||||
if cachedPriceMicros <= 0 {
|
||||
cachedPriceMicros = rule.InputPriceMicros
|
||||
}
|
||||
reasoningPriceMicros := rule.ReasoningPriceMicros
|
||||
if reasoningPriceMicros <= 0 {
|
||||
reasoningPriceMicros = rule.OutputPriceMicros
|
||||
}
|
||||
chargePrices := creditcontracts.DeriveChargePriceMicrosSet(
|
||||
rule.InputPriceMicros,
|
||||
rule.OutputPriceMicros,
|
||||
rule.CachedPriceMicros,
|
||||
rule.ReasoningPriceMicros,
|
||||
rule.ProfitRateBps,
|
||||
)
|
||||
|
||||
totalMicrosScaled := nonCachedInputTokens*maxInt64(rule.InputPriceMicros, 0) +
|
||||
cachedTokens*maxInt64(cachedPriceMicros, 0) +
|
||||
nonReasoningOutputTokens*maxInt64(rule.OutputPriceMicros, 0) +
|
||||
reasoningTokens*maxInt64(reasoningPriceMicros, 0)
|
||||
totalMicrosScaled := nonCachedInputTokens*maxInt64(chargePrices.InputChargePriceMicros, 0) +
|
||||
cachedTokens*maxInt64(chargePrices.CachedChargePriceMicros, 0) +
|
||||
nonReasoningOutputTokens*maxInt64(chargePrices.OutputChargePriceMicros, 0) +
|
||||
reasoningTokens*maxInt64(chargePrices.ReasoningChargePriceMicros, 0)
|
||||
|
||||
rmbCostMicros := ceilDivInt64(totalMicrosScaled, tokenPriceScalePer1K)
|
||||
creditCost := int64(0)
|
||||
|
||||
@@ -67,6 +67,7 @@ func ProcessUserAddTaskClassRequest(req *model.UserAddTaskClassRequest, userID i
|
||||
var items []model.TaskClassItem
|
||||
for _, itemReq := range req.Items {
|
||||
item := model.TaskClassItem{ //填充section 2
|
||||
ID: itemReq.ID,
|
||||
Order: &itemReq.Order,
|
||||
Content: &itemReq.Content,
|
||||
EmbeddedTime: itemReq.EmbeddedTime,
|
||||
|
||||
@@ -3,6 +3,7 @@ package dao
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/services/runtime/model"
|
||||
"github.com/LoveLosita/smartflow/backend/shared/respond"
|
||||
@@ -40,17 +41,23 @@ func (dao *TaskClassDAO) AddOrUpdateTaskClass(userID int, taskClass *model.TaskC
|
||||
}
|
||||
return taskClass.ID, nil
|
||||
}
|
||||
// 更新:必须同时匹配 id + user_id,否则不会更新任何行(避免覆盖他人数据)
|
||||
tx := dao.db.Model(&model.TaskClass{}).
|
||||
|
||||
// 1. 先显式校验任务类归属,避免“更新值与库内完全相同”时 RowsAffected=0 被误判成越权。
|
||||
// 2. 这里只负责校验 id/user_id 是否匹配,不负责判断具体字段有没有变化。
|
||||
// 3. 若确实不存在或不属于当前用户,统一返回 UserTaskClassForbidden。
|
||||
if err := dao.ensureTaskClassOwned(userID, taskClass.ID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 1. 更新语义是“前端提交什么,就以什么覆盖数据库当前值”。
|
||||
// 2. 因此这里不能再直接用 struct Updates,否则 nil/空切片 等零值字段会被 GORM 跳过,刷新后看起来像“没更新”。
|
||||
// 3. 统一改成显式字段映射,保证可选字段清空、排除数组清空都能真正落库。
|
||||
tx := dao.db.Model(&model.TaskClass{UserID: &userID}).
|
||||
Where("id = ? AND user_id = ?", taskClass.ID, userID).
|
||||
Updates(taskClass)
|
||||
Updates(buildTaskClassUpdateMap(taskClass))
|
||||
if tx.Error != nil {
|
||||
return 0, tx.Error
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
// 未匹配到记录:要么不存在,要么不属于该用户
|
||||
return 0, respond.UserTaskClassForbidden
|
||||
}
|
||||
return taskClass.ID, nil
|
||||
}
|
||||
|
||||
@@ -82,7 +89,27 @@ func (dao *TaskClassDAO) AddOrUpdateTaskClassItems(userID int, items []model.Tas
|
||||
return respond.UserTaskClassForbidden
|
||||
}
|
||||
|
||||
// 2) 新增与更新分开处理:新增不受影响;更新时限定 category_id(防越权)
|
||||
// 2. 收集本次要更新的已有 item,先做一次归属校验。
|
||||
// 2.1 这里单独校验是为了避免“只更新成原值”时 RowsAffected=0 被误判为越权。
|
||||
// 2.2 校验通过后,后面的 UPDATE 只关心数据库错误,不再用 RowsAffected 判权限。
|
||||
// 2.3 若请求里混入了不属于这些 task_class 的 item_id,统一返回 UserTaskClassForbidden。
|
||||
existingItemIDs := make([]int, 0, len(items))
|
||||
existingItemIDSet := make(map[int]struct{}, len(items))
|
||||
for _, it := range items {
|
||||
if it.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := existingItemIDSet[it.ID]; exists {
|
||||
continue
|
||||
}
|
||||
existingItemIDSet[it.ID] = struct{}{}
|
||||
existingItemIDs = append(existingItemIDs, it.ID)
|
||||
}
|
||||
if err := dao.ensureTaskClassItemsOwnedByCategories(existingItemIDs, categoryIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3) 新增与更新分开处理:新增直接插入;已有 item 按现有契约更新可编辑字段。
|
||||
var toCreate []model.TaskClassItem
|
||||
for _, it := range items {
|
||||
if it.ID == 0 {
|
||||
@@ -93,14 +120,14 @@ func (dao *TaskClassDAO) AddOrUpdateTaskClassItems(userID int, items []model.Tas
|
||||
tx := dao.db.Model(&model.TaskClassItem{}).
|
||||
Where("id = ? AND category_id IN ?", it.ID, categoryIDs).
|
||||
Updates(map[string]any{
|
||||
"category_id": it.CategoryID,
|
||||
"category_id": it.CategoryID,
|
||||
"order": it.Order,
|
||||
"content": it.Content,
|
||||
"embedded_time": it.EmbeddedTime,
|
||||
})
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
return respond.UserTaskClassForbidden
|
||||
}
|
||||
}
|
||||
|
||||
if len(toCreate) > 0 {
|
||||
@@ -111,6 +138,84 @@ func (dao *TaskClassDAO) AddOrUpdateTaskClassItems(userID int, items []model.Tas
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureTaskClassOwned 只负责校验 task_class 是否属于当前用户。
|
||||
func (dao *TaskClassDAO) ensureTaskClassOwned(userID int, taskClassID int) error {
|
||||
var count int64
|
||||
if err := dao.db.Model(&model.TaskClass{}).
|
||||
Where("id = ? AND user_id = ?", taskClassID, userID).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return respond.UserTaskClassForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureTaskClassItemsOwnedByCategories 只负责校验一批 item 是否都挂在允许的 task_class 下。
|
||||
func (dao *TaskClassDAO) ensureTaskClassItemsOwnedByCategories(itemIDs []int, categoryIDs []int) error {
|
||||
if len(itemIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := dao.db.Model(&model.TaskClassItem{}).
|
||||
Where("id IN ? AND category_id IN ?", itemIDs, categoryIDs).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count != int64(len(itemIDs)) {
|
||||
return respond.UserTaskClassForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildTaskClassUpdateMap 负责把“全量更新”请求转换成显式列更新。
|
||||
func buildTaskClassUpdateMap(taskClass *model.TaskClass) map[string]any {
|
||||
return map[string]any{
|
||||
"name": nullableStringValue(taskClass.Name),
|
||||
"mode": nullableStringValue(taskClass.Mode),
|
||||
"start_date": nullableTimeValue(taskClass.StartDate),
|
||||
"end_date": nullableTimeValue(taskClass.EndDate),
|
||||
"subject_type": nullableStringValue(taskClass.SubjectType),
|
||||
"difficulty_level": nullableStringValue(taskClass.DifficultyLevel),
|
||||
"cognitive_intensity": nullableStringValue(taskClass.CognitiveIntensity),
|
||||
"total_slots": nullableIntValue(taskClass.TotalSlots),
|
||||
"allow_filler_course": nullableBoolValue(taskClass.AllowFillerCourse),
|
||||
"strategy": nullableStringValue(taskClass.Strategy),
|
||||
"excluded_slots": taskClass.ExcludedSlots,
|
||||
"excluded_days_of_week": taskClass.ExcludedDaysOfWeek,
|
||||
}
|
||||
}
|
||||
|
||||
func nullableStringValue(value *string) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func nullableIntValue(value *int) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func nullableBoolValue(value *bool) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func nullableTimeValue(value *time.Time) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
// Transaction 在一个事务中执行传入的函数,供 service 层复用(自动提交/回滚)
|
||||
// 规则:fn 返回 nil -> commit;fn 返回 error 或 panic -> rollback
|
||||
func (dao *TaskClassDAO) Transaction(fn func(txDAO *TaskClassDAO) error) error {
|
||||
|
||||
@@ -264,6 +264,7 @@ func defaultCreditPriceRules() []storemodel.CreditPriceRule {
|
||||
CachedPriceMicros: 800,
|
||||
ReasoningPriceMicros: 16000,
|
||||
CreditPerYuan: 100,
|
||||
ProfitRateBps: 0,
|
||||
Status: storemodel.CreditPriceRuleStatusActive,
|
||||
Priority: 100,
|
||||
Description: "Default Ark rule, prices are expressed in micros CNY per 1K tokens.",
|
||||
|
||||
@@ -172,11 +172,12 @@ type CreditPriceRule struct {
|
||||
Scene string `gorm:"column:scene;type:varchar(64);not null;index:idx_credit_price_rules_scene_status,priority:1;comment:计费场景"`
|
||||
ProviderName string `gorm:"column:provider_name;type:varchar(64);not null;comment:模型提供方"`
|
||||
ModelName string `gorm:"column:model_name;type:varchar(128);not null;comment:模型名称"`
|
||||
InputPriceMicros int64 `gorm:"column:input_price_micros;not null;default:0;comment:输入Token单价,单位微人民币"`
|
||||
OutputPriceMicros int64 `gorm:"column:output_price_micros;not null;default:0;comment:输出Token单价,单位微人民币"`
|
||||
CachedPriceMicros int64 `gorm:"column:cached_price_micros;not null;default:0;comment:缓存Token单价,单位微人民币"`
|
||||
ReasoningPriceMicros int64 `gorm:"column:reasoning_price_micros;not null;default:0;comment:推理Token单价,单位微人民币"`
|
||||
InputPriceMicros int64 `gorm:"column:input_price_micros;not null;default:0;comment:原始输入Token成本单价,单位微人民币"`
|
||||
OutputPriceMicros int64 `gorm:"column:output_price_micros;not null;default:0;comment:原始输出Token成本单价,单位微人民币"`
|
||||
CachedPriceMicros int64 `gorm:"column:cached_price_micros;not null;default:0;comment:原始缓存Token成本单价,单位微人民币"`
|
||||
ReasoningPriceMicros int64 `gorm:"column:reasoning_price_micros;not null;default:0;comment:原始推理Token成本单价,单位微人民币"`
|
||||
CreditPerYuan int64 `gorm:"column:credit_per_yuan;not null;default:0;comment:1元人民币换算多少Credit"`
|
||||
ProfitRateBps int64 `gorm:"column:profit_rate_bps;not null;default:0;comment:在原始CNY成本基础上加价多少基点,10000=100%"`
|
||||
Status string `gorm:"column:status;type:varchar(32);not null;default:'inactive';index:idx_credit_price_rules_scene_status,priority:2;comment:active/inactive"`
|
||||
Priority int `gorm:"column:priority;not null;default:0;comment:匹配优先级,越大越优先"`
|
||||
Description string `gorm:"column:description;type:varchar(255);comment:规则说明"`
|
||||
|
||||
@@ -336,18 +336,23 @@ func creditTransactionsToPB(items []creditcontracts.CreditTransactionView) []*pb
|
||||
|
||||
func creditPriceRuleToPB(rule creditcontracts.CreditPriceRuleView) *pb.CreditPriceRuleView {
|
||||
return &pb.CreditPriceRuleView{
|
||||
RuleId: rule.RuleID,
|
||||
Scene: rule.Scene,
|
||||
ProviderName: rule.ProviderName,
|
||||
ModelName: rule.ModelName,
|
||||
InputPriceMicros: rule.InputPriceMicros,
|
||||
OutputPriceMicros: rule.OutputPriceMicros,
|
||||
CachedPriceMicros: rule.CachedPriceMicros,
|
||||
ReasoningPriceMicros: rule.ReasoningPriceMicros,
|
||||
CreditPerYuan: rule.CreditPerYuan,
|
||||
Status: rule.Status,
|
||||
Priority: int32(rule.Priority),
|
||||
Description: rule.Description,
|
||||
RuleId: rule.RuleID,
|
||||
Scene: rule.Scene,
|
||||
ProviderName: rule.ProviderName,
|
||||
ModelName: rule.ModelName,
|
||||
InputPriceMicros: rule.InputPriceMicros,
|
||||
OutputPriceMicros: rule.OutputPriceMicros,
|
||||
CachedPriceMicros: rule.CachedPriceMicros,
|
||||
ReasoningPriceMicros: rule.ReasoningPriceMicros,
|
||||
CreditPerYuan: rule.CreditPerYuan,
|
||||
ProfitRateBps: rule.ProfitRateBps,
|
||||
ChargeInputPriceMicros: rule.ChargeInputPriceMicros,
|
||||
ChargeOutputPriceMicros: rule.ChargeOutputPriceMicros,
|
||||
ChargeCachedPriceMicros: rule.ChargeCachedPriceMicros,
|
||||
ChargeReasoningPriceMicros: rule.ChargeReasoningPriceMicros,
|
||||
Status: rule.Status,
|
||||
Priority: int32(rule.Priority),
|
||||
Description: rule.Description,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -331,18 +331,23 @@ func (m *CreditTransactionView) String() string { return proto.CompactTextString
|
||||
func (*CreditTransactionView) ProtoMessage() {}
|
||||
|
||||
type CreditPriceRuleView struct {
|
||||
RuleId uint64 `protobuf:"varint,1,opt,name=rule_id,json=ruleId,proto3" json:"rule_id,omitempty"`
|
||||
Scene string `protobuf:"bytes,2,opt,name=scene,proto3" json:"scene,omitempty"`
|
||||
ProviderName string `protobuf:"bytes,3,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"`
|
||||
ModelName string `protobuf:"bytes,4,opt,name=model_name,json=modelName,proto3" json:"model_name,omitempty"`
|
||||
InputPriceMicros int64 `protobuf:"varint,5,opt,name=input_price_micros,json=inputPriceMicros,proto3" json:"input_price_micros,omitempty"`
|
||||
OutputPriceMicros int64 `protobuf:"varint,6,opt,name=output_price_micros,json=outputPriceMicros,proto3" json:"output_price_micros,omitempty"`
|
||||
CachedPriceMicros int64 `protobuf:"varint,7,opt,name=cached_price_micros,json=cachedPriceMicros,proto3" json:"cached_price_micros,omitempty"`
|
||||
ReasoningPriceMicros int64 `protobuf:"varint,8,opt,name=reasoning_price_micros,json=reasoningPriceMicros,proto3" json:"reasoning_price_micros,omitempty"`
|
||||
CreditPerYuan int64 `protobuf:"varint,9,opt,name=credit_per_yuan,json=creditPerYuan,proto3" json:"credit_per_yuan,omitempty"`
|
||||
Status string `protobuf:"bytes,10,opt,name=status,proto3" json:"status,omitempty"`
|
||||
Priority int32 `protobuf:"varint,11,opt,name=priority,proto3" json:"priority,omitempty"`
|
||||
Description string `protobuf:"bytes,12,opt,name=description,proto3" json:"description,omitempty"`
|
||||
RuleId uint64 `protobuf:"varint,1,opt,name=rule_id,json=ruleId,proto3" json:"rule_id,omitempty"`
|
||||
Scene string `protobuf:"bytes,2,opt,name=scene,proto3" json:"scene,omitempty"`
|
||||
ProviderName string `protobuf:"bytes,3,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"`
|
||||
ModelName string `protobuf:"bytes,4,opt,name=model_name,json=modelName,proto3" json:"model_name,omitempty"`
|
||||
InputPriceMicros int64 `protobuf:"varint,5,opt,name=input_price_micros,json=inputPriceMicros,proto3" json:"input_price_micros,omitempty"`
|
||||
OutputPriceMicros int64 `protobuf:"varint,6,opt,name=output_price_micros,json=outputPriceMicros,proto3" json:"output_price_micros,omitempty"`
|
||||
CachedPriceMicros int64 `protobuf:"varint,7,opt,name=cached_price_micros,json=cachedPriceMicros,proto3" json:"cached_price_micros,omitempty"`
|
||||
ReasoningPriceMicros int64 `protobuf:"varint,8,opt,name=reasoning_price_micros,json=reasoningPriceMicros,proto3" json:"reasoning_price_micros,omitempty"`
|
||||
CreditPerYuan int64 `protobuf:"varint,9,opt,name=credit_per_yuan,json=creditPerYuan,proto3" json:"credit_per_yuan,omitempty"`
|
||||
Status string `protobuf:"bytes,10,opt,name=status,proto3" json:"status,omitempty"`
|
||||
Priority int32 `protobuf:"varint,11,opt,name=priority,proto3" json:"priority,omitempty"`
|
||||
Description string `protobuf:"bytes,12,opt,name=description,proto3" json:"description,omitempty"`
|
||||
ProfitRateBps int64 `protobuf:"varint,13,opt,name=profit_rate_bps,json=profitRateBps,proto3" json:"profit_rate_bps,omitempty"`
|
||||
ChargeInputPriceMicros int64 `protobuf:"varint,14,opt,name=charge_input_price_micros,json=chargeInputPriceMicros,proto3" json:"charge_input_price_micros,omitempty"`
|
||||
ChargeOutputPriceMicros int64 `protobuf:"varint,15,opt,name=charge_output_price_micros,json=chargeOutputPriceMicros,proto3" json:"charge_output_price_micros,omitempty"`
|
||||
ChargeCachedPriceMicros int64 `protobuf:"varint,16,opt,name=charge_cached_price_micros,json=chargeCachedPriceMicros,proto3" json:"charge_cached_price_micros,omitempty"`
|
||||
ChargeReasoningPriceMicros int64 `protobuf:"varint,17,opt,name=charge_reasoning_price_micros,json=chargeReasoningPriceMicros,proto3" json:"charge_reasoning_price_micros,omitempty"`
|
||||
}
|
||||
|
||||
func (m *CreditPriceRuleView) Reset() { *m = CreditPriceRuleView{} }
|
||||
|
||||
@@ -235,6 +235,11 @@ message CreditPriceRuleView {
|
||||
string status = 10;
|
||||
int32 priority = 11;
|
||||
string description = 12;
|
||||
int64 profit_rate_bps = 13;
|
||||
int64 charge_input_price_micros = 14;
|
||||
int64 charge_output_price_micros = 15;
|
||||
int64 charge_cached_price_micros = 16;
|
||||
int64 charge_reasoning_price_micros = 17;
|
||||
}
|
||||
|
||||
message CreditRewardRuleView {
|
||||
|
||||
@@ -114,19 +114,31 @@ func creditTransactionViewFromModel(ledger storemodel.CreditLedger) creditcontra
|
||||
}
|
||||
|
||||
func creditPriceRuleViewFromModel(rule storemodel.CreditPriceRule) creditcontracts.CreditPriceRuleView {
|
||||
chargePrices := creditcontracts.DeriveChargePriceMicrosSet(
|
||||
rule.InputPriceMicros,
|
||||
rule.OutputPriceMicros,
|
||||
rule.CachedPriceMicros,
|
||||
rule.ReasoningPriceMicros,
|
||||
rule.ProfitRateBps,
|
||||
)
|
||||
return creditcontracts.CreditPriceRuleView{
|
||||
RuleID: rule.ID,
|
||||
Scene: rule.Scene,
|
||||
ProviderName: rule.ProviderName,
|
||||
ModelName: rule.ModelName,
|
||||
InputPriceMicros: rule.InputPriceMicros,
|
||||
OutputPriceMicros: rule.OutputPriceMicros,
|
||||
CachedPriceMicros: rule.CachedPriceMicros,
|
||||
ReasoningPriceMicros: rule.ReasoningPriceMicros,
|
||||
CreditPerYuan: rule.CreditPerYuan,
|
||||
Status: rule.Status,
|
||||
Priority: rule.Priority,
|
||||
Description: rule.Description,
|
||||
RuleID: rule.ID,
|
||||
Scene: rule.Scene,
|
||||
ProviderName: rule.ProviderName,
|
||||
ModelName: rule.ModelName,
|
||||
InputPriceMicros: rule.InputPriceMicros,
|
||||
OutputPriceMicros: rule.OutputPriceMicros,
|
||||
CachedPriceMicros: rule.CachedPriceMicros,
|
||||
ReasoningPriceMicros: rule.ReasoningPriceMicros,
|
||||
CreditPerYuan: rule.CreditPerYuan,
|
||||
ProfitRateBps: rule.ProfitRateBps,
|
||||
ChargeInputPriceMicros: chargePrices.InputChargePriceMicros,
|
||||
ChargeOutputPriceMicros: chargePrices.OutputChargePriceMicros,
|
||||
ChargeCachedPriceMicros: chargePrices.CachedChargePriceMicros,
|
||||
ChargeReasoningPriceMicros: chargePrices.ReasoningChargePriceMicros,
|
||||
Status: rule.Status,
|
||||
Priority: rule.Priority,
|
||||
Description: rule.Description,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
57
backend/shared/contracts/creditstore/pricing.go
Normal file
57
backend/shared/contracts/creditstore/pricing.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package creditstore
|
||||
|
||||
const (
|
||||
// ProfitRateScaleBPS 表示利润率的基点精度:10000 = 100%。
|
||||
ProfitRateScaleBPS = int64(10_000)
|
||||
)
|
||||
|
||||
// ChargePriceMicrosSet 表示在“原始 CNY 成本 + 利润率”之后得到的实际计费单价。
|
||||
type ChargePriceMicrosSet struct {
|
||||
InputChargePriceMicros int64
|
||||
OutputChargePriceMicros int64
|
||||
CachedChargePriceMicros int64
|
||||
ReasoningChargePriceMicros int64
|
||||
}
|
||||
|
||||
// DeriveChargePriceMicrosSet 负责把一条价格规则里的原始 CNY 成本推导成实际计费用单价。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只处理“原始成本 + 利润率”的价格推导,不负责 token 数量聚合与 credit 折算。
|
||||
// 2. cached/reasoning 仍复用现有兜底语义:未配置时分别退回 input/output 单价。
|
||||
// 3. 若利润率配置为负且绝对值过大导致结果 <= 0,则统一按 0 处理,避免落出负价格。
|
||||
func DeriveChargePriceMicrosSet(inputPriceMicros, outputPriceMicros, cachedPriceMicros, reasoningPriceMicros, profitRateBps int64) ChargePriceMicrosSet {
|
||||
if cachedPriceMicros <= 0 {
|
||||
cachedPriceMicros = inputPriceMicros
|
||||
}
|
||||
if reasoningPriceMicros <= 0 {
|
||||
reasoningPriceMicros = outputPriceMicros
|
||||
}
|
||||
|
||||
return ChargePriceMicrosSet{
|
||||
InputChargePriceMicros: ApplyProfitRateToPriceMicros(inputPriceMicros, profitRateBps),
|
||||
OutputChargePriceMicros: ApplyProfitRateToPriceMicros(outputPriceMicros, profitRateBps),
|
||||
CachedChargePriceMicros: ApplyProfitRateToPriceMicros(cachedPriceMicros, profitRateBps),
|
||||
ReasoningChargePriceMicros: ApplyProfitRateToPriceMicros(reasoningPriceMicros, profitRateBps),
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyProfitRateToPriceMicros 负责把“成本单价”按利润率转换成“实际计费单价”。
|
||||
func ApplyProfitRateToPriceMicros(priceMicros int64, profitRateBps int64) int64 {
|
||||
if priceMicros <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
scaledRate := ProfitRateScaleBPS + profitRateBps
|
||||
if scaledRate <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return ceilDivInt64(priceMicros*scaledRate, ProfitRateScaleBPS)
|
||||
}
|
||||
|
||||
func ceilDivInt64(numerator int64, denominator int64) int64 {
|
||||
if numerator <= 0 || denominator <= 0 {
|
||||
return 0
|
||||
}
|
||||
return (numerator + denominator - 1) / denominator
|
||||
}
|
||||
@@ -93,19 +93,30 @@ type CreditConsumptionDashboardView struct {
|
||||
}
|
||||
|
||||
// CreditPriceRuleView 是 Credit 计价规则展示结构。
|
||||
//
|
||||
// 字段语义说明:
|
||||
// 1. InputPriceMicros / OutputPriceMicros / CachedPriceMicros / ReasoningPriceMicros
|
||||
// 表示原始 CNY 成本单价,方便管理端直接维护模型底价。
|
||||
// 2. Charge*PriceMicros 表示后端按利润率自动推导出来的实际计费单价,
|
||||
// LLM 扣费时会使用这一组价格继续换算 credit。
|
||||
type CreditPriceRuleView struct {
|
||||
RuleID uint64 `json:"rule_id"`
|
||||
Scene string `json:"scene"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
ModelName string `json:"model_name"`
|
||||
InputPriceMicros int64 `json:"input_price_micros"`
|
||||
OutputPriceMicros int64 `json:"output_price_micros"`
|
||||
CachedPriceMicros int64 `json:"cached_price_micros"`
|
||||
ReasoningPriceMicros int64 `json:"reasoning_price_micros"`
|
||||
CreditPerYuan int64 `json:"credit_per_yuan"`
|
||||
Status string `json:"status"`
|
||||
Priority int `json:"priority"`
|
||||
Description string `json:"description"`
|
||||
RuleID uint64 `json:"rule_id"`
|
||||
Scene string `json:"scene"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
ModelName string `json:"model_name"`
|
||||
InputPriceMicros int64 `json:"input_price_micros"`
|
||||
OutputPriceMicros int64 `json:"output_price_micros"`
|
||||
CachedPriceMicros int64 `json:"cached_price_micros"`
|
||||
ReasoningPriceMicros int64 `json:"reasoning_price_micros"`
|
||||
CreditPerYuan int64 `json:"credit_per_yuan"`
|
||||
ProfitRateBps int64 `json:"profit_rate_bps"`
|
||||
ChargeInputPriceMicros int64 `json:"charge_input_price_micros"`
|
||||
ChargeOutputPriceMicros int64 `json:"charge_output_price_micros"`
|
||||
ChargeCachedPriceMicros int64 `json:"charge_cached_price_micros"`
|
||||
ChargeReasoningPriceMicros int64 `json:"charge_reasoning_price_micros"`
|
||||
Status string `json:"status"`
|
||||
Priority int `json:"priority"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// CreditRewardRuleView 是 Credit 奖励规则展示结构。
|
||||
|
||||
@@ -69,6 +69,13 @@ func (p *GormCachePlugin) dispatchCacheLogic(modelObj interface{}) {
|
||||
case model.Schedule:
|
||||
p.invalidScheduleCache(m.UserID, m.Week)
|
||||
case model.TaskClass:
|
||||
// 1. update 场景若只用 Model(&model.TaskClass{}) 构造 SQL,插件拿到的模型实例里 UserID 可能为空。
|
||||
// 2. 这类情况下缓存插件不能影响主事务,更不能因为取缓存键时解引用空指针把服务打崩。
|
||||
// 3. 若确实缺少 UserID,则直接跳过本次失效,由调用方补齐 Model 上下文或后续重读兜底。
|
||||
if m.UserID == nil {
|
||||
log.Printf("[GORM-Cache] Skip task class cache invalidation because UserID is nil")
|
||||
return
|
||||
}
|
||||
p.invalidTaskClassCache(*m.UserID)
|
||||
case model.Task:
|
||||
p.invalidTaskCache(m.UserID)
|
||||
|
||||
Reference in New Issue
Block a user