feat(pricing): OpenOrder/SaveConfig 按 subject 类型校验 + per_token 按模型实时价结算
- service 层全量切换 subject_type+subject_id 键(去除 BizKey 残留) - OpenOrder 按 subject 类型 resolveChargeMode + 规则快照 + 门禁 + 幂等 - SaveConfig/GetConfig 按 subject 类型校验 rules 结构 - settleOrder 支持 per_token 分支(calcPerTokenOrder 按模型实时价) - 修复 model 计算器:P1 nil price 校验、P2 unit/base 交叉校验、M3 cached>prompt 负成本钳位 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -291,9 +291,19 @@ func (c modelTokenCalculator) validate(rulesJSON string) (interface{}, error) {
|
||||
if r.Unit != pricingConsts.ChargeModePer1K && r.Unit != pricingConsts.ChargeModePer1M {
|
||||
return nil, errors.New("model token 费率 unit 须为 per_1K/per_1M")
|
||||
}
|
||||
// 实例基准与 JSON unit 交叉校验:per_1K 实例 base=1000,per_1M 实例 base=1e6,
|
||||
// 防止 per_1M 配置配 per_1K 单位导致结算 1000 倍价差(P2)。
|
||||
if (c.base == 1000) != (r.Unit == pricingConsts.ChargeModePer1K) {
|
||||
return nil, errors.New("model token 费率 unit 与实例基准不匹配")
|
||||
}
|
||||
if len(r.Rules) == 0 {
|
||||
return nil, errors.New("model 费率至少一条 rules")
|
||||
}
|
||||
for i := range r.Rules {
|
||||
if r.Rules[i].Price == nil {
|
||||
return nil, errors.New("model 费率规则须配置 price")
|
||||
}
|
||||
}
|
||||
if r.Tiered {
|
||||
if err := validateTieredBands(r.Rules); err != nil {
|
||||
return nil, err
|
||||
@@ -325,7 +335,12 @@ func (c modelTokenCalculator) charge(rules interface{}, usage *ChargeUsage) (flo
|
||||
return 0, errors.New("无匹配计费规则")
|
||||
}
|
||||
p := rule.Price
|
||||
cost := float64(usage.PromptTokens-usage.CachedTokens)/c.base*p.Input +
|
||||
// 防负成本:异常/边界用量报 cached > prompt 时钳为 0,避免结算出现负数(M3)。
|
||||
promptInput := usage.PromptTokens - usage.CachedTokens
|
||||
if promptInput < 0 {
|
||||
promptInput = 0
|
||||
}
|
||||
cost := float64(promptInput)/c.base*p.Input +
|
||||
float64(usage.CachedTokens)/c.base*p.CacheHit +
|
||||
float64(usage.CompletionTokens)/c.base*p.Output
|
||||
return ceilFen(roundCost(cost)), nil
|
||||
@@ -353,6 +368,11 @@ func (c modelUnitCalculator) validate(rulesJSON string) (interface{}, error) {
|
||||
if len(r.Rules) == 0 {
|
||||
return nil, errors.New("model 费率至少一条 rules")
|
||||
}
|
||||
for i := range r.Rules {
|
||||
if r.Rules[i].Price == nil {
|
||||
return nil, errors.New("model 费率规则须配置 price")
|
||||
}
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -130,3 +130,60 @@ func TestCalcChargeBySubjectType(t *testing.T) {
|
||||
t.Fatalf("model per_second got %v want 1.98, err %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
// P1:validate 拒绝缺 price 的规则(token 计算器)
|
||||
func TestModelTokenCalculator_ValidateNilPrice(t *testing.T) {
|
||||
c := modelTokenCalculator{base: 1e6}
|
||||
if _, err := c.validate(`{"unit":"per_1M","tiered":false,"rules":[{"name":"无价"}]}`); err == nil {
|
||||
t.Fatal("expected nil price validation error")
|
||||
}
|
||||
}
|
||||
|
||||
// P1:validate 拒绝缺 price 的规则(单位计算器)
|
||||
func TestModelUnitCalculator_ValidateNilPrice(t *testing.T) {
|
||||
c := modelUnitCalculator{base: 60, usage: usageDurationSec}
|
||||
if _, err := c.validate(`{"unit":"per_minute","tiered":false,"rules":[{"name":"无价"}]}`); err == nil {
|
||||
t.Fatal("expected nil price validation error")
|
||||
}
|
||||
}
|
||||
|
||||
// P2:modelTokenCalculator.validate 拒绝 unit 与实例 base 不匹配的配置
|
||||
func TestModelTokenCalculator_ValidateUnitBaseMismatch(t *testing.T) {
|
||||
// per_1M 实例(base=1e6)配 per_1K 单位 → 校验失败
|
||||
c := modelTokenCalculator{base: 1e6}
|
||||
if _, err := c.validate(`{"unit":"per_1K","tiered":false,"rules":[{"name":"统一","price":{"input":1}}]}`); err == nil {
|
||||
t.Fatal("expected unit/base mismatch error for per_1M instance with per_1K unit")
|
||||
}
|
||||
// per_1K 实例(base=1000)配 per_1M 单位 → 校验失败
|
||||
c2 := modelTokenCalculator{base: 1000}
|
||||
if _, err := c2.validate(`{"unit":"per_1M","tiered":false,"rules":[{"name":"统一","price":{"input":1}}]}`); err == nil {
|
||||
t.Fatal("expected unit/base mismatch error for per_1K instance with per_1M unit")
|
||||
}
|
||||
// 匹配时不报错
|
||||
if _, err := c.validate(`{"unit":"per_1M","tiered":false,"rules":[{"name":"统一","price":{"input":1}}]}`); err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if _, err := c2.validate(`{"unit":"per_1K","tiered":false,"rules":[{"name":"统一","price":{"input":1}}]}`); err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// M3:cached > prompt 时金额不为负(promptInput 钳为 0)
|
||||
func TestModelTokenCalculator_CachedGreaterThanPrompt(t *testing.T) {
|
||||
c := modelTokenCalculator{base: 1000}
|
||||
rules, err := c.validate(`{"unit":"per_1K","tiered":false,"rules":[{"name":"统一","price":{"input":1,"output":2,"cacheHit":0.1}}]}`)
|
||||
if err != nil {
|
||||
t.Fatalf("validate err: %v", err)
|
||||
}
|
||||
// cached 3000 > prompt 1000 → promptInput 钳 0:0 + 3000/1000*0.1 + 0 = 0.3
|
||||
got, err := c.charge(rules, &ChargeUsage{PromptTokens: 1000, CompletionTokens: 0, CachedTokens: 3000})
|
||||
if err != nil {
|
||||
t.Fatalf("charge err: %v", err)
|
||||
}
|
||||
if got < 0 {
|
||||
t.Fatalf("got negative amount %v", got)
|
||||
}
|
||||
if got != 0.3 {
|
||||
t.Fatalf("got %v want 0.3", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
var (
|
||||
// ErrOrderAlreadyHandled 计费单已被处理(并发幂等:让事务回滚,避免账务重复)
|
||||
ErrOrderAlreadyHandled = errors.New("计费单已被处理")
|
||||
// ErrConfigNotFound 计价配置不存在(service 层业务错误)
|
||||
ErrConfigNotFound = errors.New("计价配置不存在")
|
||||
)
|
||||
|
||||
// pricing 计价服务:算钱(定价+计费单生命周期+门禁)。
|
||||
@@ -36,28 +38,36 @@ type pricing struct{}
|
||||
|
||||
// ====================== 建单(不动钱) ======================
|
||||
|
||||
// OpenOrder 建单:校验配置 + 门禁(余额 >= min_balance,非冻结),建 CREATED 计费单,不动钱。
|
||||
// 幂等键 biz_key + biz_order_no(工作流 execId),重复调用返回既有单。
|
||||
// OpenOrder 建单:校验配置 + 解析计费方式 + 门禁(余额 >= min_balance,非冻结),建 CREATED 计费单,不动钱。
|
||||
// 幂等键 subject_type + subject_id + biz_order_no(工作流 execId),重复调用返回既有单。
|
||||
func (s *pricing) OpenOrder(ctx context.Context, req *pricingDto.OpenOrderReq) (*pricingDto.ChargeOrderInfo, error) {
|
||||
cfg, err := s.getEnabledConfig(ctx, req.BizKey)
|
||||
cfg, err := s.getEnabledConfig(ctx, pricingConsts.SubjectType(req.SubjectType), req.SubjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 幂等快路径:同 biz_key + biz_order_no 已建单直接返回
|
||||
if exist, err := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{BizKey: req.BizKey, BizOrderNo: req.BizOrderNo}); err != nil {
|
||||
// 幂等快路径:同 subject + biz_order_no 已建单直接返回
|
||||
if exist, err := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{
|
||||
SubjectType: req.SubjectType, SubjectID: req.SubjectID, BizOrderNo: req.BizOrderNo,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
} else if exist != nil {
|
||||
info := s.toOrderInfo(exist)
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// 建单前校验费率 JSON(脏配置在建单即暴露,不拖到结算)
|
||||
c, err := getCalculator(pricingConsts.SubjectTypeWorkflow, cfg.ChargeMode)
|
||||
// 解析计费方式与规则快照(含 chargeMode 校验)
|
||||
chargeMode, ruleSnapshot, err := s.resolveChargeMode(ctx, cfg, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = c.validate(cfg.Rules); err != nil {
|
||||
|
||||
// 建单前校验费率 JSON(脏配置在建单即暴露,不拖到结算)
|
||||
c, err := getCalculator(cfg.SubjectType, chargeMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = c.validate(ruleSnapshot); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -78,28 +88,32 @@ func (s *pricing) OpenOrder(ctx context.Context, req *pricingDto.OpenOrderReq) (
|
||||
}
|
||||
|
||||
id, err := chargeDao.ChargeOrder.Insert(ctx, &pricingDto.CreateChargeOrderReq{
|
||||
BizKey: req.BizKey,
|
||||
SubjectType: cfg.SubjectType,
|
||||
SubjectID: cfg.SubjectID,
|
||||
BizOrderNo: req.BizOrderNo,
|
||||
UserId: req.UserId,
|
||||
ChargeMode: cfg.ChargeMode,
|
||||
ChargeMode: chargeMode,
|
||||
Status: pricingConsts.ChargeOrderStatusCreated,
|
||||
RuleSnapshot: cfg.Rules, // 费率快照,结算据此计价,防改价影响在途单
|
||||
RuleSnapshot: ruleSnapshot, // 费率快照,结算据此计价,防改价影响在途单
|
||||
})
|
||||
if err != nil {
|
||||
// 唯一键冲突:并发已建单,幂等返回既有单
|
||||
if exist, e2 := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{BizKey: req.BizKey, BizOrderNo: req.BizOrderNo}); e2 == nil && exist != nil {
|
||||
if exist, e2 := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{
|
||||
SubjectType: req.SubjectType, SubjectID: req.SubjectID, BizOrderNo: req.BizOrderNo,
|
||||
}); e2 == nil && exist != nil {
|
||||
info := s.toOrderInfo(exist)
|
||||
return &info, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
entity := &pricingEntity.ChargeOrder{
|
||||
BizKey: req.BizKey,
|
||||
SubjectType: cfg.SubjectType,
|
||||
SubjectID: cfg.SubjectID,
|
||||
BizOrderNo: req.BizOrderNo,
|
||||
UserId: req.UserId,
|
||||
ChargeMode: cfg.ChargeMode,
|
||||
ChargeMode: chargeMode,
|
||||
Status: pricingConsts.ChargeOrderStatusCreated,
|
||||
RuleSnapshot: cfg.Rules,
|
||||
RuleSnapshot: ruleSnapshot,
|
||||
}
|
||||
entity.Id = id
|
||||
info := s.toOrderInfo(entity)
|
||||
@@ -110,7 +124,7 @@ func (s *pricing) OpenOrder(ctx context.Context, req *pricingDto.OpenOrderReq) (
|
||||
|
||||
// Settle 结算:按实际用量计价实收(余额不足允许为负=欠费),幂等。
|
||||
func (s *pricing) Settle(ctx context.Context, req *pricingDto.SettleReq) (*pricingDto.ChargeOrderInfo, error) {
|
||||
order, err := s.resolveOrder(ctx, req.OrderId, req.BizKey, req.BizOrderNo)
|
||||
order, err := s.resolveOrder(ctx, req.OrderId, req.SubjectType, req.SubjectID, req.BizOrderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -123,7 +137,7 @@ func (s *pricing) Settle(ctx context.Context, req *pricingDto.SettleReq) (*prici
|
||||
|
||||
// Cancel 取消:中途取消按已消耗用量计价实收(非退款),幂等。
|
||||
func (s *pricing) Cancel(ctx context.Context, req *pricingDto.CancelReq) (*pricingDto.ChargeOrderInfo, error) {
|
||||
order, err := s.resolveOrder(ctx, req.OrderId, req.BizKey, req.BizOrderNo)
|
||||
order, err := s.resolveOrder(ctx, req.OrderId, req.SubjectType, req.SubjectID, req.BizOrderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -149,8 +163,14 @@ func (s *pricing) settleOrder(ctx context.Context, order *pricingEntity.ChargeOr
|
||||
if err := json.Unmarshal([]byte(usageJSON), &usage); err != nil {
|
||||
return 0, fmt.Errorf("用量JSON解析失败: %v", err)
|
||||
}
|
||||
// 用建单时费率快照计价(防改价影响在途单)
|
||||
actualAmount, err := calcCharge(pricingConsts.SubjectTypeWorkflow, order.ChargeMode, order.RuleSnapshot, &usage)
|
||||
// 用建单时费率快照计价(防改价影响在途单);per_token 特殊:按各模型 subject 实时价(spec §5.4)
|
||||
var actualAmount float64
|
||||
var err error
|
||||
if order.SubjectType == pricingConsts.SubjectTypeWorkflow && order.ChargeMode == pricingConsts.ChargeModePerToken {
|
||||
actualAmount, err = s.calcPerTokenOrder(ctx, &usage)
|
||||
} else {
|
||||
actualAmount, err = calcCharge(order.SubjectType, order.ChargeMode, order.RuleSnapshot, &usage)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -165,7 +185,7 @@ func (s *pricing) settleOrder(ctx context.Context, order *pricingEntity.ChargeOr
|
||||
Type: walletConsts.WalletLogTypeExpense,
|
||||
Amount: actualAmount,
|
||||
OrderNo: fmt.Sprintf("charge:%d", order.Id),
|
||||
Description: desc + order.BizKey,
|
||||
Description: desc + string(order.SubjectType) + ":" + order.SubjectID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -195,11 +215,45 @@ func (s *pricing) settleOrder(ctx context.Context, order *pricingEntity.ChargeOr
|
||||
return actualAmount, nil
|
||||
}
|
||||
|
||||
// calcPerTokenOrder 工作流 per_token 结算:按 usage.TokensByModel 逐模型查该模型 subject 配置计价累加。
|
||||
// 未配价/未启用模型按 0 计(不收费、不报错);费率不冻结,结算按模型实时价(spec §5.4)。
|
||||
func (s *pricing) calcPerTokenOrder(ctx context.Context, usage *ChargeUsage) (float64, error) {
|
||||
var total float64
|
||||
for modelID, tokens := range usage.TokensByModel {
|
||||
if tokens <= 0 {
|
||||
continue
|
||||
}
|
||||
cfg, err := chargeDao.PricingConfig.Get(ctx, &pricingDto.GetConfigReq{
|
||||
SubjectType: string(pricingConsts.SubjectTypeModel), SubjectID: modelID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if cfg == nil || cfg.Enabled != 1 {
|
||||
continue // 未配价模型按 0 计
|
||||
}
|
||||
c, err := getCalculator(cfg.SubjectType, cfg.Unit())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rules, err := c.validate(cfg.Rules)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
amount, err := c.charge(rules, &ChargeUsage{PromptTokens: tokens, CompletionTokens: 0, CachedTokens: 0})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
total += amount
|
||||
}
|
||||
return ceilFen(total), nil
|
||||
}
|
||||
|
||||
// ====================== 失败(不扣费) ======================
|
||||
|
||||
// Fail 失败处理:执行失败/无产出,不扣费、不动钱,仅状态迁移 CREATED → FAILED。幂等。
|
||||
func (s *pricing) Fail(ctx context.Context, req *pricingDto.FailReq) (*pricingDto.ChargeOrderInfo, error) {
|
||||
order, err := s.resolveOrder(ctx, req.OrderId, req.BizKey, req.BizOrderNo)
|
||||
order, err := s.resolveOrder(ctx, req.OrderId, req.SubjectType, req.SubjectID, req.BizOrderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -226,9 +280,9 @@ func (s *pricing) Fail(ctx context.Context, req *pricingDto.FailReq) (*pricingDt
|
||||
|
||||
// ====================== 查询 ======================
|
||||
|
||||
// GetOrder 查询计费单(按ID或 biz_key + biz_order_no)
|
||||
// GetOrder 查询计费单(按ID或 subject_type + subject_id + biz_order_no)
|
||||
func (s *pricing) GetOrder(ctx context.Context, req *pricingDto.GetOrderReq) (*pricingDto.ChargeOrderInfo, error) {
|
||||
order, err := s.resolveOrder(ctx, req.OrderId, req.BizKey, req.BizOrderNo)
|
||||
order, err := s.resolveOrder(ctx, req.OrderId, req.SubjectType, req.SubjectID, req.BizOrderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -254,14 +308,10 @@ func (s *pricing) ListOrders(ctx context.Context, req *pricingDto.ListOrdersReq)
|
||||
|
||||
// ====================== 配置管理 ======================
|
||||
|
||||
// SaveConfig 新增/更新计价配置(费率全 DB 配置;同 biz_key 唯一)
|
||||
// SaveConfig 新增/更新计价配置(费率全 DB 配置;按 subject 类型校验 rules 结构,防止脏配置上线后结算报错)
|
||||
func (s *pricing) SaveConfig(ctx context.Context, req *pricingDto.SaveConfigReq) (*pricingDto.SaveConfigData, error) {
|
||||
c, err := getCalculator(pricingConsts.SubjectTypeWorkflow, pricingConsts.ChargeMode(req.ChargeMode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 保存前校验费率 JSON 结构(防止脏配置上线后结算报错)
|
||||
if _, err = c.validate(req.Rules); err != nil {
|
||||
subjectType := pricingConsts.SubjectType(req.SubjectType)
|
||||
if err := s.validateConfigRules(subjectType, req.Rules); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Currency == "" {
|
||||
@@ -277,6 +327,59 @@ func (s *pricing) SaveConfig(ctx context.Context, req *pricingDto.SaveConfigReq)
|
||||
return &pricingDto.SaveConfigData{Id: id}, nil
|
||||
}
|
||||
|
||||
// validateConfigRules 按 subject 类型校验 rules JSON(spec §4):
|
||||
// workflow:三模式容器逐个校验已配模式;model:先解析 unit 再用对应计算器;business:per_period 计算器
|
||||
func (s *pricing) validateConfigRules(subjectType pricingConsts.SubjectType, rulesJSON string) error {
|
||||
switch subjectType {
|
||||
case pricingConsts.SubjectTypeWorkflow:
|
||||
return s.validateWorkflowRules(rulesJSON)
|
||||
case pricingConsts.SubjectTypeModel:
|
||||
var cfg struct {
|
||||
Unit pricingConsts.ChargeMode `json:"unit"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(rulesJSON), &cfg); err != nil {
|
||||
return fmt.Errorf("model 费率JSON解析失败: %v", err)
|
||||
}
|
||||
c, err := getCalculator(subjectType, cfg.Unit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.validate(rulesJSON)
|
||||
return err
|
||||
case pricingConsts.SubjectTypeBusiness:
|
||||
c, err := getCalculator(subjectType, pricingConsts.ChargeModePerPeriod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.validate(rulesJSON)
|
||||
return err
|
||||
default:
|
||||
return errors.New("未知计价对象类型: " + string(subjectType))
|
||||
}
|
||||
}
|
||||
|
||||
// validateWorkflowRules 校验三模式容器:逐个校验已配模式(spec §4.1)
|
||||
func (s *pricing) validateWorkflowRules(rulesJSON string) error {
|
||||
var container map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(rulesJSON), &container); err != nil {
|
||||
return fmt.Errorf("workflow 费率JSON解析失败: %v", err)
|
||||
}
|
||||
if len(container) == 0 {
|
||||
return errors.New("workflow 费率至少配置一种模式")
|
||||
}
|
||||
for mode, raw := range container {
|
||||
chargeMode := pricingConsts.ChargeMode(mode)
|
||||
c, err := getCalculator(pricingConsts.SubjectTypeWorkflow, chargeMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = c.validate(string(raw)); err != nil {
|
||||
return fmt.Errorf("workflow %s 模式校验失败: %v", mode, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig 查询计价配置
|
||||
func (s *pricing) GetConfig(ctx context.Context, req *pricingDto.GetConfigReq) (*pricingDto.PricingConfigInfo, error) {
|
||||
cfg, err := chargeDao.PricingConfig.Get(ctx, req)
|
||||
@@ -284,7 +387,7 @@ func (s *pricing) GetConfig(ctx context.Context, req *pricingDto.GetConfigReq) (
|
||||
return nil, err
|
||||
}
|
||||
if cfg == nil {
|
||||
return nil, errors.New("计价配置不存在: " + req.BizKey)
|
||||
return nil, ErrConfigNotFound
|
||||
}
|
||||
info := s.toConfigInfo(cfg)
|
||||
return &info, nil
|
||||
@@ -306,22 +409,56 @@ func (s *pricing) ListConfigs(ctx context.Context, req *pricingDto.ListConfigsRe
|
||||
// ====================== 内部辅助 ======================
|
||||
|
||||
// getEnabledConfig 获取启用中的计价配置
|
||||
func (s *pricing) getEnabledConfig(ctx context.Context, bizKey string) (*pricingEntity.PricingConfig, error) {
|
||||
cfg, err := chargeDao.PricingConfig.Get(ctx, &pricingDto.GetConfigReq{BizKey: bizKey})
|
||||
func (s *pricing) getEnabledConfig(ctx context.Context, subjectType pricingConsts.SubjectType, subjectID string) (*pricingEntity.PricingConfig, error) {
|
||||
cfg, err := chargeDao.PricingConfig.Get(ctx, &pricingDto.GetConfigReq{SubjectType: string(subjectType), SubjectID: subjectID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg == nil {
|
||||
return nil, errors.New("计价配置不存在: " + bizKey)
|
||||
return nil, ErrConfigNotFound
|
||||
}
|
||||
if cfg.Enabled != 1 {
|
||||
return nil, errors.New("计价配置未启用: " + bizKey)
|
||||
return nil, errors.New("计价配置未启用: " + subjectID)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// resolveOrder 按ID或 biz_key + biz_order_no 定位计费单
|
||||
func (s *pricing) resolveOrder(ctx context.Context, orderId int64, bizKey, bizOrderNo string) (*pricingEntity.ChargeOrder, error) {
|
||||
// resolveChargeMode 建单时解析实际计费方式与规则快照(spec §6 OpenOrder 校验):
|
||||
// workflow:chargeMode 必填 ∈ rules 已配模式;快照 = rules[chargeMode]
|
||||
// model:chargeMode 可空 = config.unit;快照 = 整个 modelCfg
|
||||
// business:chargeMode 可空 = per_period;快照 = 周期规则
|
||||
func (s *pricing) resolveChargeMode(ctx context.Context, cfg *pricingEntity.PricingConfig, req *pricingDto.OpenOrderReq) (pricingConsts.ChargeMode, string, error) {
|
||||
switch cfg.SubjectType {
|
||||
case pricingConsts.SubjectTypeWorkflow:
|
||||
if req.ChargeMode == "" {
|
||||
return "", "", errors.New("workflow 建单须指定 chargeMode")
|
||||
}
|
||||
return pricingConsts.ChargeMode(req.ChargeMode), s.workflowRuleSnapshot(cfg.Rules, req.ChargeMode), nil
|
||||
case pricingConsts.SubjectTypeModel:
|
||||
// model 计费方式固定取配置 unit(cfg.Unit() 见 entity,Task 3 定义)
|
||||
return cfg.Unit(), cfg.Rules, nil
|
||||
case pricingConsts.SubjectTypeBusiness:
|
||||
return pricingConsts.ChargeModePerPeriod, cfg.Rules, nil
|
||||
default:
|
||||
return "", "", errors.New("未知计价对象类型: " + string(cfg.SubjectType))
|
||||
}
|
||||
}
|
||||
|
||||
// workflowRuleSnapshot 从三模式容器取选中模式的规则片段(spec §4.1)
|
||||
func (s *pricing) workflowRuleSnapshot(rulesJSON, mode string) string {
|
||||
var container map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(rulesJSON), &container); err != nil {
|
||||
return rulesJSON
|
||||
}
|
||||
raw, ok := container[mode]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// resolveOrder 按ID或 subject_type+subject_id+biz_order_no 定位计费单
|
||||
func (s *pricing) resolveOrder(ctx context.Context, orderId int64, subjectType, subjectID, bizOrderNo string) (*pricingEntity.ChargeOrder, error) {
|
||||
if orderId > 0 {
|
||||
order, err := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{Id: orderId})
|
||||
if err != nil {
|
||||
@@ -332,10 +469,10 @@ func (s *pricing) resolveOrder(ctx context.Context, orderId int64, bizKey, bizOr
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
if bizKey == "" || bizOrderNo == "" {
|
||||
return nil, errors.New("orderId 与 bizKey/bizOrderNo 须二选一")
|
||||
if subjectType == "" || subjectID == "" || bizOrderNo == "" {
|
||||
return nil, errors.New("orderId 与 subjectType/subjectId/bizOrderNo 须二选一")
|
||||
}
|
||||
order, err := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{BizKey: bizKey, BizOrderNo: bizOrderNo})
|
||||
order, err := chargeDao.ChargeOrder.GetOrder(ctx, &pricingDto.GetChargeOrderReq{SubjectType: subjectType, SubjectID: subjectID, BizOrderNo: bizOrderNo})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -349,7 +486,8 @@ func (s *pricing) resolveOrder(ctx context.Context, orderId int64, bizKey, bizOr
|
||||
func (s *pricing) toOrderInfo(e *pricingEntity.ChargeOrder) pricingDto.ChargeOrderInfo {
|
||||
info := pricingDto.ChargeOrderInfo{
|
||||
ID: e.Id,
|
||||
BizKey: e.BizKey,
|
||||
SubjectType: string(e.SubjectType),
|
||||
SubjectID: e.SubjectID,
|
||||
BizOrderNo: e.BizOrderNo,
|
||||
UserId: e.UserId,
|
||||
ChargeMode: string(e.ChargeMode),
|
||||
@@ -370,15 +508,14 @@ func (s *pricing) toOrderInfo(e *pricingEntity.ChargeOrder) pricingDto.ChargeOrd
|
||||
// toConfigInfo 计价配置实体转响应
|
||||
func (s *pricing) toConfigInfo(e *pricingEntity.PricingConfig) pricingDto.PricingConfigInfo {
|
||||
info := pricingDto.PricingConfigInfo{
|
||||
ID: e.Id,
|
||||
BizKey: e.BizKey,
|
||||
Name: e.Name,
|
||||
ChargeMode: string(e.ChargeMode),
|
||||
Rules: e.Rules,
|
||||
MinBalance: e.MinBalance,
|
||||
Currency: e.Currency,
|
||||
Enabled: e.Enabled,
|
||||
Version: e.Version,
|
||||
ID: e.Id,
|
||||
SubjectType: string(e.SubjectType),
|
||||
SubjectID: e.SubjectID,
|
||||
Rules: e.Rules,
|
||||
MinBalance: e.MinBalance,
|
||||
Currency: e.Currency,
|
||||
Enabled: e.Enabled,
|
||||
Version: e.Version,
|
||||
}
|
||||
if e.CreatedAt != nil {
|
||||
info.CreatedAt = e.CreatedAt.String()
|
||||
|
||||
Reference in New Issue
Block a user