111 lines
4.6 KiB
Go
111 lines
4.6 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
|
||
commonHttp "gitea.redpowerfuture.com/red-future/common/http"
|
||
"gitea.redpowerfuture.com/red-future/common/utils"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
)
|
||
|
||
// ====================== shop-user-trade 计价对接(独立 module,本地 JSON 对齐) ======================
|
||
|
||
// shopPricingConfig shop-user-trade config/get 响应(仅取启用标记与最低余额门限;费率规则在 shop-user-trade 侧,model-gateway 不消费)
|
||
type shopPricingConfig struct {
|
||
Enabled int `json:"enabled"`
|
||
MinBalance float64 `json:"minBalance" dc:"门禁:调用前可用余额须>=该值(元),0=不校验"`
|
||
}
|
||
|
||
// shopWalletAccount shop-user-trade wallet/account/get 响应
|
||
type shopWalletAccount struct {
|
||
ID int64 `json:"id"`
|
||
UserID int64 `json:"userId"`
|
||
Balance float64 `json:"balance" dc:"可用余额(元),负值=欠费"`
|
||
Currency string `json:"currency"`
|
||
Status int `json:"status" dc:"状态:1启用 0禁用 -1冻结"`
|
||
}
|
||
|
||
// shopCalcFeeRes shop-user-trade /calc 响应
|
||
type shopCalcFeeRes struct {
|
||
Cost float64 `json:"cost"`
|
||
}
|
||
|
||
// pricingURL 组装 shop-user-trade 计价接口地址(common http.RouteRegister 推导前缀,同 ai-agent billing.go)
|
||
func pricingURL(sub string) string {
|
||
return "shop-user-trade/pricing/controller/" + sub
|
||
}
|
||
|
||
// walletURL 组装 shop-user-trade 钱包接口地址(accountController → account/controller,与 pricing 同 RouteRegister 推导规则)
|
||
func walletURL(sub string) string {
|
||
return "shop-user-trade/account/controller/" + sub
|
||
}
|
||
|
||
// modelBillable 调用前门禁:模型须在 shop-user-trade 已配置且启用计价,否则阻塞调用。
|
||
// 替换原 CheckTenantBalance(admin-go 租户余额门禁);未配置→config/get 返回错误,未启用→enabled!=1。
|
||
// minBalance>0 时追加最低余额门禁:钱包须存在且可用余额 >= 门限(与 shop-user-trade open_order 同语义,0=不校验)。
|
||
func modelBillable(ctx context.Context, modelId int64) error {
|
||
var cfg shopPricingConfig
|
||
err := commonHttp.Get(ctx, pricingURL("config/get"), utils.HeadersFromCtx(ctx, utils.HeadersOptions{ResolveToken: true}), &cfg,
|
||
"subjectType", "model", "subjectId", fmt.Sprintf("%d", modelId))
|
||
if err != nil {
|
||
return fmt.Errorf("模型未配置计价,无法调用: %w", err)
|
||
}
|
||
if cfg.Enabled != 1 {
|
||
return fmt.Errorf("模型未启用计价,无法调用")
|
||
}
|
||
if cfg.MinBalance <= 0 {
|
||
return nil
|
||
}
|
||
user, e := utils.GetUserInfo(ctx)
|
||
if e != nil || user == nil || user.Id == 0 {
|
||
return fmt.Errorf("取不到用户,无法校验最低余额")
|
||
}
|
||
var acc shopWalletAccount
|
||
if e = commonHttp.Get(ctx, walletURL("get"), utils.HeadersFromCtx(ctx, utils.HeadersOptions{ResolveToken: true}), &acc,
|
||
"userId", fmt.Sprintf("%d", user.Id)); e != nil {
|
||
return fmt.Errorf("获取钱包失败,无法校验最低余额: %w", e)
|
||
}
|
||
if acc.Status != 1 {
|
||
return fmt.Errorf("钱包不可用,无法调用")
|
||
}
|
||
if acc.Balance < cfg.MinBalance {
|
||
return fmt.Errorf("余额不足:可用余额须不低于 %.2f 元才能发起", cfg.MinBalance)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// buildModelUsage 组装算费用量 JSON 对象(ChargeUsage 形状)。mediaType 为 shop 计费词汇
|
||
// (audio/video,空=无媒体引用走默认价;DetectMediaType/异步快照已直接为该词汇,不再二次转换)。
|
||
// per_char 模型把输出字数映射到 completionTokens(TokenMapping),随该字段传给 shop /calc 计价。
|
||
func buildModelUsage(prompt, completion, cached int64, mediaType string, durationSec int64) map[string]any {
|
||
if durationSec < 0 {
|
||
durationSec = 0
|
||
}
|
||
return map[string]any{
|
||
"promptTokens": prompt,
|
||
"completionTokens": completion,
|
||
"cachedTokens": cached,
|
||
"mediaType": mediaType,
|
||
"durationSec": durationSec,
|
||
}
|
||
}
|
||
|
||
// calcModelCost 调 shop-user-trade /calc 按用量算费(不建单不扣费)。
|
||
// 调用前门禁已保证配置存在;此处失败(配置中途删除/网络抖动)→ 记日志返回 0,不拖垮已完成的模型调用。
|
||
func calcModelCost(ctx context.Context, modelId int64, usage map[string]any) float64 {
|
||
var res shopCalcFeeRes
|
||
err := commonHttp.Post(ctx, pricingURL("calc"), utils.HeadersFromCtx(ctx, utils.HeadersOptions{ResolveToken: true}), &res, &struct {
|
||
SubjectType string `json:"subjectType"`
|
||
SubjectID string `json:"subjectId"`
|
||
Usage map[string]any `json:"usage"`
|
||
}{
|
||
SubjectType: "model", SubjectID: fmt.Sprintf("%d", modelId), Usage: usage,
|
||
})
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "[算费] 调用 shop-user-trade 失败 modelId=%d: %v", modelId, err)
|
||
return 0
|
||
}
|
||
return res.Cost
|
||
}
|