From 0ec970a182922645f2688fb29cfd669bea68a876 Mon Sep 17 00:00:00 2001 From: chengqian Date: Wed, 12 Aug 2026 17:15:27 +0800 Subject: [PATCH] =?UTF-8?q?improve(ui):=E6=96=B0=E5=A2=9E=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=B9=BF=E5=9C=BA=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/tenant/tenant.go | 2 + api/tenant/v1/marketplace.go | 42 + .../tenant/tenant_v1_marketplace.go | 15 + internal/dao/bil_daily_usage_summary.go | 22 + internal/dao/bil_monthly_usage_summary.go | 22 + internal/dao/internal/aud_request_logs.go | 2 - .../dao/internal/bil_daily_usage_summary.go | 93 + .../dao/internal/bil_monthly_usage_summary.go | 93 + internal/dao/internal/bil_usage_daily.go | 8 +- internal/dao/internal/mdl_tenant_models.go | 2 +- internal/logic/tenant/marketplace.go | 201 ++ internal/model/do/aud_request_logs.go | 1 - internal/model/do/bil_daily_usage_summary.go | 23 + .../model/do/bil_monthly_usage_summary.go | 23 + internal/model/do/bil_usage_daily.go | 4 +- internal/model/do/mdl_tenant_models.go | 2 +- internal/model/entity/aud_request_logs.go | 1 - .../model/entity/bil_daily_usage_summary.go | 22 + .../model/entity/bil_monthly_usage_summary.go | 22 + internal/model/entity/bil_usage_daily.go | 4 +- internal/model/entity/mdl_tenant_models.go | 2 +- internal/service/tenant.go | 4 + web/tenant/src/api/marketplace.ts | 36 + web/tenant/src/router/tenant.ts | 6 + web/tenant/src/utils/renderUtils.ts | 4 +- web/tenant/src/utils/request.ts | 9 + web/tenant/src/views/landing/LandingPage.vue | 20 + .../src/views/marketplace/MarketplacePage.vue | 2632 +++++++++++++++++ 28 files changed, 3301 insertions(+), 16 deletions(-) create mode 100644 api/tenant/v1/marketplace.go create mode 100644 internal/controller/tenant/tenant_v1_marketplace.go create mode 100644 internal/dao/bil_daily_usage_summary.go create mode 100644 internal/dao/bil_monthly_usage_summary.go create mode 100644 internal/dao/internal/bil_daily_usage_summary.go create mode 100644 internal/dao/internal/bil_monthly_usage_summary.go create mode 100644 internal/logic/tenant/marketplace.go create mode 100644 internal/model/do/bil_daily_usage_summary.go create mode 100644 internal/model/do/bil_monthly_usage_summary.go create mode 100644 internal/model/entity/bil_daily_usage_summary.go create mode 100644 internal/model/entity/bil_monthly_usage_summary.go create mode 100644 web/tenant/src/api/marketplace.ts create mode 100644 web/tenant/src/views/marketplace/MarketplacePage.vue diff --git a/api/tenant/tenant.go b/api/tenant/tenant.go index f4c36cb..1ebe8f4 100644 --- a/api/tenant/tenant.go +++ b/api/tenant/tenant.go @@ -57,6 +57,8 @@ type ITenantV1 interface { HelpPublicArticleGet(ctx context.Context, req *v1.HelpPublicArticleGetReq) (res *v1.HelpPublicArticleGetRes, err error) HelpPublicSearch(ctx context.Context, req *v1.HelpPublicSearchReq) (res *v1.HelpPublicSearchRes, err error) TenantLevelBenefits(ctx context.Context, req *v1.TenantLevelBenefitsReq) (res *v1.TenantLevelBenefitsRes, err error) + MarketplaceList(ctx context.Context, req *v1.MarketplaceListReq) (res *v1.MarketplaceListRes, err error) + MarketplaceDetail(ctx context.Context, req *v1.MarketplaceDetailReq) (res *v1.MarketplaceDetailRes, err error) TenantMemberList(ctx context.Context, req *v1.TenantMemberListReq) (res *v1.TenantMemberListRes, err error) TenantMemberInvite(ctx context.Context, req *v1.TenantMemberInviteReq) (res *v1.TenantMemberInviteRes, err error) TenantInvitationList(ctx context.Context, req *v1.TenantInvitationListReq) (res *v1.TenantInvitationListRes, err error) diff --git a/api/tenant/v1/marketplace.go b/api/tenant/v1/marketplace.go new file mode 100644 index 0000000..e557bb9 --- /dev/null +++ b/api/tenant/v1/marketplace.go @@ -0,0 +1,42 @@ +package v1 + +import ( + "github.com/gogf/gf/v2/frame/g" +) + +type MarketplaceListReq struct { + g.Meta `path:"/marketplace/models" method:"get" tags:"公开-模型广场" summary:"获取模型广场列表" group:"public" middleware:"-"` + Keyword string `json:"keyword" dc:"搜索关键词"` + Category string `json:"category" dc:"模型类别筛选(chat/embedding/image/audio等)"` + Page int `json:"page" d:"1" dc:"页码"` + PageSize int `json:"page_size" d:"20" v:"between:1,100" dc:"每页数量"` +} + +type MarketplaceListRes struct { + List []MarketplaceModelItem `json:"list"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +type MarketplaceModelItem struct { + ModelId string `json:"model_id"` // 模型标识 + ModelName string `json:"model_name"` // 模型显示名称 + Category string `json:"category"` // 分类(chat/embedding/image等) + Description string `json:"description"` // 描述 + MaxContextTokens int `json:"max_context_tokens"` // 最大上下文 tokens + MaxOutputTokens int `json:"max_output_tokens"` // 最大输出 tokens + InputPrice float64 `json:"input_price"` // 输入价格(每百万 token,USD) + OutputPrice float64 `json:"output_price"` // 输出价格(每百万 token,USD) + Tags []string `json:"tags"` // 标签 + Capabilities g.Map `json:"capabilities"` // 能力标签 +} + +type MarketplaceDetailReq struct { + g.Meta `path:"/marketplace/models/{model_id}" method:"get" tags:"公开-模型广场" summary:"获取模型详情" group:"public" middleware:"-"` + ModelId string `json:"model_id" in:"path" v:"required" dc:"模型标识"` +} + +type MarketplaceDetailRes struct { + MarketplaceModelItem +} diff --git a/internal/controller/tenant/tenant_v1_marketplace.go b/internal/controller/tenant/tenant_v1_marketplace.go new file mode 100644 index 0000000..aa0a5e4 --- /dev/null +++ b/internal/controller/tenant/tenant_v1_marketplace.go @@ -0,0 +1,15 @@ +package tenant + +import ( + "context" + + "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/service" +) + +func (c *ControllerV1) MarketplaceList(ctx context.Context, req *v1.MarketplaceListReq) (res *v1.MarketplaceListRes, err error) { + return service.Tenant().GetModelList(ctx, req) +} +func (c *ControllerV1) MarketplaceDetail(ctx context.Context, req *v1.MarketplaceDetailReq) (res *v1.MarketplaceDetailRes, err error) { + return service.Tenant().GetModelDetail(ctx, req) +} diff --git a/internal/dao/bil_daily_usage_summary.go b/internal/dao/bil_daily_usage_summary.go new file mode 100644 index 0000000..17ea30a --- /dev/null +++ b/internal/dao/bil_daily_usage_summary.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "github.com/qianfree/team-api/internal/dao/internal" +) + +// bilDailyUsageSummaryDao is the data access object for the table bil_daily_usage_summary. +// You can define custom methods on it to extend its functionality as needed. +type bilDailyUsageSummaryDao struct { + *internal.BilDailyUsageSummaryDao +} + +var ( + // BilDailyUsageSummary is a globally accessible object for table bil_daily_usage_summary operations. + BilDailyUsageSummary = bilDailyUsageSummaryDao{internal.NewBilDailyUsageSummaryDao()} +) + +// Add your custom methods and functionality below. diff --git a/internal/dao/bil_monthly_usage_summary.go b/internal/dao/bil_monthly_usage_summary.go new file mode 100644 index 0000000..cc8f12a --- /dev/null +++ b/internal/dao/bil_monthly_usage_summary.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "github.com/qianfree/team-api/internal/dao/internal" +) + +// bilMonthlyUsageSummaryDao is the data access object for the table bil_monthly_usage_summary. +// You can define custom methods on it to extend its functionality as needed. +type bilMonthlyUsageSummaryDao struct { + *internal.BilMonthlyUsageSummaryDao +} + +var ( + // BilMonthlyUsageSummary is a globally accessible object for table bil_monthly_usage_summary operations. + BilMonthlyUsageSummary = bilMonthlyUsageSummaryDao{internal.NewBilMonthlyUsageSummaryDao()} +) + +// Add your custom methods and functionality below. diff --git a/internal/dao/internal/aud_request_logs.go b/internal/dao/internal/aud_request_logs.go index 8cb94c2..f62e8d1 100644 --- a/internal/dao/internal/aud_request_logs.go +++ b/internal/dao/internal/aud_request_logs.go @@ -51,7 +51,6 @@ type AudRequestLogsColumns struct { TaskResult string // 异步任务完成时上游返回的原始响应体 TaskUpstreamHeaders string // 异步任务完成时上游返回的响应头(仅审计级别为 full 时记录) TaskCompletedAt string // 异步任务达到终态的时间 - AttachmentIds string // 提取的媒体附件 fil_files ID 数组(JSONB: [1,2,3]) } // audRequestLogsColumns holds the columns for the table aud_request_logs. @@ -86,7 +85,6 @@ var audRequestLogsColumns = AudRequestLogsColumns{ TaskResult: "task_result", TaskUpstreamHeaders: "task_upstream_headers", TaskCompletedAt: "task_completed_at", - AttachmentIds: "attachment_ids", } // NewAudRequestLogsDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/bil_daily_usage_summary.go b/internal/dao/internal/bil_daily_usage_summary.go new file mode 100644 index 0000000..168ca51 --- /dev/null +++ b/internal/dao/internal/bil_daily_usage_summary.go @@ -0,0 +1,93 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// BilDailyUsageSummaryDao is the data access object for the table bil_daily_usage_summary. +type BilDailyUsageSummaryDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns BilDailyUsageSummaryColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// BilDailyUsageSummaryColumns defines and stores column names for the table bil_daily_usage_summary. +type BilDailyUsageSummaryColumns struct { + Id string // + TenantId string // + Date string // + TotalRequests string // + TotalTokens string // + TotalCost string // + CreatedAt string // + UpdatedAt string // +} + +// bilDailyUsageSummaryColumns holds the columns for the table bil_daily_usage_summary. +var bilDailyUsageSummaryColumns = BilDailyUsageSummaryColumns{ + Id: "id", + TenantId: "tenant_id", + Date: "date", + TotalRequests: "total_requests", + TotalTokens: "total_tokens", + TotalCost: "total_cost", + CreatedAt: "created_at", + UpdatedAt: "updated_at", +} + +// NewBilDailyUsageSummaryDao creates and returns a new DAO object for table data access. +func NewBilDailyUsageSummaryDao(handlers ...gdb.ModelHandler) *BilDailyUsageSummaryDao { + return &BilDailyUsageSummaryDao{ + group: "default", + table: "bil_daily_usage_summary", + columns: bilDailyUsageSummaryColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *BilDailyUsageSummaryDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *BilDailyUsageSummaryDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *BilDailyUsageSummaryDao) Columns() BilDailyUsageSummaryColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *BilDailyUsageSummaryDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *BilDailyUsageSummaryDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *BilDailyUsageSummaryDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/bil_monthly_usage_summary.go b/internal/dao/internal/bil_monthly_usage_summary.go new file mode 100644 index 0000000..ad43219 --- /dev/null +++ b/internal/dao/internal/bil_monthly_usage_summary.go @@ -0,0 +1,93 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// BilMonthlyUsageSummaryDao is the data access object for the table bil_monthly_usage_summary. +type BilMonthlyUsageSummaryDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns BilMonthlyUsageSummaryColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// BilMonthlyUsageSummaryColumns defines and stores column names for the table bil_monthly_usage_summary. +type BilMonthlyUsageSummaryColumns struct { + Id string // + TenantId string // + Month string // + TotalRequests string // + TotalTokens string // + TotalCost string // + CreatedAt string // + UpdatedAt string // +} + +// bilMonthlyUsageSummaryColumns holds the columns for the table bil_monthly_usage_summary. +var bilMonthlyUsageSummaryColumns = BilMonthlyUsageSummaryColumns{ + Id: "id", + TenantId: "tenant_id", + Month: "month", + TotalRequests: "total_requests", + TotalTokens: "total_tokens", + TotalCost: "total_cost", + CreatedAt: "created_at", + UpdatedAt: "updated_at", +} + +// NewBilMonthlyUsageSummaryDao creates and returns a new DAO object for table data access. +func NewBilMonthlyUsageSummaryDao(handlers ...gdb.ModelHandler) *BilMonthlyUsageSummaryDao { + return &BilMonthlyUsageSummaryDao{ + group: "default", + table: "bil_monthly_usage_summary", + columns: bilMonthlyUsageSummaryColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *BilMonthlyUsageSummaryDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *BilMonthlyUsageSummaryDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *BilMonthlyUsageSummaryDao) Columns() BilMonthlyUsageSummaryColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *BilMonthlyUsageSummaryDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *BilMonthlyUsageSummaryDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *BilMonthlyUsageSummaryDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/bil_usage_daily.go b/internal/dao/internal/bil_usage_daily.go index 6a4b1ed..3b2ceb0 100644 --- a/internal/dao/internal/bil_usage_daily.go +++ b/internal/dao/internal/bil_usage_daily.go @@ -33,10 +33,10 @@ type BilUsageDailyColumns struct { OutputTokens string // 输出Token合计 TotalCost string // 客户侧成本合计(USD,源自 bil_usage_logs.total_cost) AccountCost string // 上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析) - CreatedAt string // - UpdatedAt string // SumLatencyMs string // 总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值) SumFirstTokenMs string // 首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值) + CreatedAt string // + UpdatedAt string // } // bilUsageDailyColumns holds the columns for the table bil_usage_daily. @@ -53,10 +53,10 @@ var bilUsageDailyColumns = BilUsageDailyColumns{ OutputTokens: "output_tokens", TotalCost: "total_cost", AccountCost: "account_cost", - CreatedAt: "created_at", - UpdatedAt: "updated_at", SumLatencyMs: "sum_latency_ms", SumFirstTokenMs: "sum_first_token_ms", + CreatedAt: "created_at", + UpdatedAt: "updated_at", } // NewBilUsageDailyDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/mdl_tenant_models.go b/internal/dao/internal/mdl_tenant_models.go index 885b674..4f603b1 100644 --- a/internal/dao/internal/mdl_tenant_models.go +++ b/internal/dao/internal/mdl_tenant_models.go @@ -27,7 +27,7 @@ type MdlTenantModelsColumns struct { Enabled string // 是否启用(禁用后该租户无法调用此模型) CustomInputPrice string // 租户自定义输入价格(NULL 表示使用默认定价) CustomOutputPrice string // 租户自定义输出价格(NULL 表示使用默认定价) - Multiplier string // 租户价格倍率(VIP 折扣)。作为 tenant_multiplier 参与最终价格计算:最终价格 = 基础价格 × 租户倍率。倍率来源优先级:discount_ratio > multiplier > 租户等级 price_multiplier + Multiplier string // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率) CreatedAt string // 创建时间 UpdatedAt string // 更新时间 BillingMode string // 覆盖模型计费方式(NULL表示跟随模型默认) diff --git a/internal/logic/tenant/marketplace.go b/internal/logic/tenant/marketplace.go new file mode 100644 index 0000000..145dd64 --- /dev/null +++ b/internal/logic/tenant/marketplace.go @@ -0,0 +1,201 @@ +package tenant + +import ( + "context" + "fmt" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + + v1 "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/dao" + "github.com/qianfree/team-api/internal/logic/billing" + "github.com/qianfree/team-api/internal/model/entity" +) + +// GetModelList 获取模型广场列表(从默认模型分组加载) +func (s *sTenant) GetModelList(ctx context.Context, req *v1.MarketplaceListReq) (*v1.MarketplaceListRes, error) { + // 1. 查找默认模型分组 + var defaultGroup *entity.MdlModelGroups + err := dao.MdlModelGroups.Ctx(ctx). + Where("is_default", true). + Where("status", "active"). + Scan(&defaultGroup) + if err != nil { + return nil, err + } + if defaultGroup == nil { + // 没有默认分组,返回空列表 + return &v1.MarketplaceListRes{ + List: []v1.MarketplaceModelItem{}, + Total: 0, + Page: req.Page, + PageSize: req.PageSize, + }, nil + } + + // 2. 构建查询:通过分组关联查询模型 + m := dao.MdlModels.Ctx(ctx). + LeftJoin("mdl_group_models", "mdl_group_models.model_id = mdl_models.id"). + Where("mdl_group_models.group_id", defaultGroup.Id). + Where("mdl_models.status", "active") + + // 搜索关键词 + if req.Keyword != "" { + keyword := "%" + req.Keyword + "%" + m = m.Where("(mdl_models.model_id LIKE ? OR mdl_models.model_name LIKE ? OR mdl_models.description LIKE ?)", keyword, keyword, keyword) + } + + // 分类筛选 + if req.Category != "" { + m = m.Where("mdl_models.category", req.Category) + } + + // 排序:按类别、模型名称 + m = m.OrderAsc("mdl_models.category").OrderAsc("mdl_models.model_id") + + // 3. 查询总数 + total, err := m.Count() + if err != nil { + return nil, err + } + + // 4. 分页查询模型 + var models []*entity.MdlModels + err = m.Fields("mdl_models.*"). + Page(req.Page, req.PageSize). + Scan(&models) + if err != nil { + return nil, err + } + + // 5. 批量查询价格(从 mdl_pricing 表) + if len(models) == 0 { + return &v1.MarketplaceListRes{ + List: []v1.MarketplaceModelItem{}, + Total: 0, + Page: req.Page, + PageSize: req.PageSize, + }, nil + } + + modelIds := make([]int64, len(models)) + for i, model := range models { + modelIds[i] = model.Id + } + + var pricings []*entity.MdlPricing + err = dao.MdlPricing.Ctx(ctx). + WhereIn("model_id", modelIds). + Where("billing_mode", "token"). // 只取 token 计费模式的基础价格 + Scan(&pricings) + if err != nil { + return nil, err + } + + // 构建价格映射(model_id -> pricing) + priceMap := make(map[int64]*entity.MdlPricing) + for _, pricing := range pricings { + priceMap[pricing.ModelId] = pricing + } + + // 6. 转换为响应格式 + list := make([]v1.MarketplaceModelItem, 0, len(models)) + for _, model := range models { + item := s.convertToMarketplaceItem(ctx, model, priceMap[model.Id]) + list = append(list, item) + } + + return &v1.MarketplaceListRes{ + List: list, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, nil +} + +// GetModelDetail 获取模型详情 +func (s *sTenant) GetModelDetail(ctx context.Context, req *v1.MarketplaceDetailReq) (*v1.MarketplaceDetailRes, error) { + // 1. 检查模型是否在默认分组中 + var defaultGroup *entity.MdlModelGroups + err := dao.MdlModelGroups.Ctx(ctx). + Where("is_default", true). + Where("status", "active"). + Scan(&defaultGroup) + if err != nil { + return nil, err + } + if defaultGroup == nil { + return nil, fmt.Errorf("默认模型分组不存在") + } + + // 2. 查询模型(需在默认分组中) + var model *entity.MdlModels + err = dao.MdlModels.Ctx(ctx). + LeftJoin("mdl_group_models", "mdl_group_models.model_id = mdl_models.id"). + Where("mdl_group_models.group_id", defaultGroup.Id). + Where("mdl_models.model_id", req.ModelId). + Where("mdl_models.status", "active"). + Fields("mdl_models.*"). + Scan(&model) + if err != nil { + return nil, err + } + if model == nil { + return nil, fmt.Errorf("模型不存在或未公开") + } + + // 3. 查询价格 + var pricing *entity.MdlPricing + err = dao.MdlPricing.Ctx(ctx). + Where("model_id", model.Id). + Where("billing_mode", "token"). + Scan(&pricing) + if err != nil { + return nil, err + } + + // 4. 转换为响应格式 + item := s.convertToMarketplaceItem(ctx, model, pricing) + + return &v1.MarketplaceDetailRes{ + MarketplaceModelItem: item, + }, nil +} + +// convertToMarketplaceItem 转换模型实体为响应格式 +func (s *sTenant) convertToMarketplaceItem(ctx context.Context, model *entity.MdlModels, pricing *entity.MdlPricing) v1.MarketplaceModelItem { + item := v1.MarketplaceModelItem{ + ModelId: model.ModelId, + ModelName: model.ModelName, + Category: model.Category, + Description: model.Description, + MaxContextTokens: model.MaxContextTokens, + MaxOutputTokens: model.MaxOutputTokens, + Tags: []string{}, + Capabilities: g.Map{}, + } + + // 标签 + if model.Tags != nil && len(model.Tags) > 0 { + item.Tags = model.Tags + } + + // 能力标签(从 capabilities JSONB 解析) + if model.Capabilities != "" { + var capabilities map[string]interface{} + if err := gconv.Struct(model.Capabilities, &capabilities); err == nil { + item.Capabilities = capabilities + } + } + + // 价格(数据库已存储每百万 token 的美元价格,直接返回) + if pricing != nil { + // input_price/output_price 数据库字段存储的就是每 1M token 价格(USD) + // 无需转换,直接使用 + item.InputPrice = billing.InexactFloat64(pricing.InputPrice) + item.OutputPrice = billing.InexactFloat64(pricing.OutputPrice) + } + + return item +} diff --git a/internal/model/do/aud_request_logs.go b/internal/model/do/aud_request_logs.go index 6d6dbbd..3cd909a 100644 --- a/internal/model/do/aud_request_logs.go +++ b/internal/model/do/aud_request_logs.go @@ -42,5 +42,4 @@ type AudRequestLogs struct { TaskResult any // 异步任务完成时上游返回的原始响应体 TaskUpstreamHeaders any // 异步任务完成时上游返回的响应头(仅审计级别为 full 时记录) TaskCompletedAt *gtime.Time // 异步任务达到终态的时间 - AttachmentIds any // 提取的媒体附件 fil_files ID 数组(JSONB: [1,2,3]) } diff --git a/internal/model/do/bil_daily_usage_summary.go b/internal/model/do/bil_daily_usage_summary.go new file mode 100644 index 0000000..ae0789d --- /dev/null +++ b/internal/model/do/bil_daily_usage_summary.go @@ -0,0 +1,23 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// BilDailyUsageSummary is the golang structure of table bil_daily_usage_summary for DAO operations like Where/Data. +type BilDailyUsageSummary struct { + g.Meta `orm:"table:bil_daily_usage_summary, do:true"` + Id any // + TenantId any // + Date *gtime.Time // + TotalRequests any // + TotalTokens any // + TotalCost any // + CreatedAt *gtime.Time // + UpdatedAt *gtime.Time // +} diff --git a/internal/model/do/bil_monthly_usage_summary.go b/internal/model/do/bil_monthly_usage_summary.go new file mode 100644 index 0000000..3609021 --- /dev/null +++ b/internal/model/do/bil_monthly_usage_summary.go @@ -0,0 +1,23 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// BilMonthlyUsageSummary is the golang structure of table bil_monthly_usage_summary for DAO operations like Where/Data. +type BilMonthlyUsageSummary struct { + g.Meta `orm:"table:bil_monthly_usage_summary, do:true"` + Id any // + TenantId any // + Month *gtime.Time // + TotalRequests any // + TotalTokens any // + TotalCost any // + CreatedAt *gtime.Time // + UpdatedAt *gtime.Time // +} diff --git a/internal/model/do/bil_usage_daily.go b/internal/model/do/bil_usage_daily.go index 91d82e2..4a5efc8 100644 --- a/internal/model/do/bil_usage_daily.go +++ b/internal/model/do/bil_usage_daily.go @@ -24,8 +24,8 @@ type BilUsageDaily struct { OutputTokens any // 输出Token合计 TotalCost any // 客户侧成本合计(USD,源自 bil_usage_logs.total_cost) AccountCost any // 上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析) - CreatedAt *gtime.Time // - UpdatedAt *gtime.Time // SumLatencyMs any // 总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值) SumFirstTokenMs any // 首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值) + CreatedAt *gtime.Time // + UpdatedAt *gtime.Time // } diff --git a/internal/model/do/mdl_tenant_models.go b/internal/model/do/mdl_tenant_models.go index e510c84..c849def 100644 --- a/internal/model/do/mdl_tenant_models.go +++ b/internal/model/do/mdl_tenant_models.go @@ -19,7 +19,7 @@ type MdlTenantModels struct { Enabled any // 是否启用(禁用后该租户无法调用此模型) CustomInputPrice *decimal.Decimal // 租户自定义输入价格(NULL 表示使用默认定价) CustomOutputPrice *decimal.Decimal // 租户自定义输出价格(NULL 表示使用默认定价) - Multiplier any // 租户价格倍率(VIP 折扣)。作为 tenant_multiplier 参与最终价格计算:最终价格 = 基础价格 × 租户倍率。倍率来源优先级:discount_ratio > multiplier > 租户等级 price_multiplier + Multiplier any // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率) CreatedAt *gtime.Time // 创建时间 UpdatedAt *gtime.Time // 更新时间 BillingMode any // 覆盖模型计费方式(NULL表示跟随模型默认) diff --git a/internal/model/entity/aud_request_logs.go b/internal/model/entity/aud_request_logs.go index bf20994..dc48e93 100644 --- a/internal/model/entity/aud_request_logs.go +++ b/internal/model/entity/aud_request_logs.go @@ -40,5 +40,4 @@ type AudRequestLogs struct { TaskResult string `json:"task_result" orm:"task_result" description:"异步任务完成时上游返回的原始响应体"` // 异步任务完成时上游返回的原始响应体 TaskUpstreamHeaders string `json:"task_upstream_headers" orm:"task_upstream_headers" description:"异步任务完成时上游返回的响应头(仅审计级别为 full 时记录)"` // 异步任务完成时上游返回的响应头(仅审计级别为 full 时记录) TaskCompletedAt *gtime.Time `json:"task_completed_at" orm:"task_completed_at" description:"异步任务达到终态的时间"` // 异步任务达到终态的时间 - AttachmentIds string `json:"attachment_ids" orm:"attachment_ids" description:"提取的媒体附件 fil_files ID 数组(JSONB: [1,2,3])"` // 提取的媒体附件 fil_files ID 数组(JSONB: [1,2,3]) } diff --git a/internal/model/entity/bil_daily_usage_summary.go b/internal/model/entity/bil_daily_usage_summary.go new file mode 100644 index 0000000..56acba9 --- /dev/null +++ b/internal/model/entity/bil_daily_usage_summary.go @@ -0,0 +1,22 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/shopspring/decimal" +) + +// BilDailyUsageSummary is the golang structure for table bil_daily_usage_summary. +type BilDailyUsageSummary struct { + Id int64 `json:"id" orm:"id" description:""` // + TenantId int64 `json:"tenant_id" orm:"tenant_id" description:""` // + Date *gtime.Time `json:"date" orm:"date" description:""` // + TotalRequests int `json:"total_requests" orm:"total_requests" description:""` // + TotalTokens int64 `json:"total_tokens" orm:"total_tokens" description:""` // + TotalCost decimal.Decimal `json:"total_cost" orm:"total_cost" description:""` // + CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` // + UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` // +} diff --git a/internal/model/entity/bil_monthly_usage_summary.go b/internal/model/entity/bil_monthly_usage_summary.go new file mode 100644 index 0000000..219530b --- /dev/null +++ b/internal/model/entity/bil_monthly_usage_summary.go @@ -0,0 +1,22 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" + "github.com/shopspring/decimal" +) + +// BilMonthlyUsageSummary is the golang structure for table bil_monthly_usage_summary. +type BilMonthlyUsageSummary struct { + Id int64 `json:"id" orm:"id" description:""` // + TenantId int64 `json:"tenant_id" orm:"tenant_id" description:""` // + Month *gtime.Time `json:"month" orm:"month" description:""` // + TotalRequests int `json:"total_requests" orm:"total_requests" description:""` // + TotalTokens int64 `json:"total_tokens" orm:"total_tokens" description:""` // + TotalCost decimal.Decimal `json:"total_cost" orm:"total_cost" description:""` // + CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` // + UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` // +} diff --git a/internal/model/entity/bil_usage_daily.go b/internal/model/entity/bil_usage_daily.go index e26cbba..96a3937 100644 --- a/internal/model/entity/bil_usage_daily.go +++ b/internal/model/entity/bil_usage_daily.go @@ -23,8 +23,8 @@ type BilUsageDaily struct { OutputTokens int64 `json:"output_tokens" orm:"output_tokens" description:"输出Token合计"` // 输出Token合计 TotalCost decimal.Decimal `json:"total_cost" orm:"total_cost" description:"客户侧成本合计(USD,源自 bil_usage_logs.total_cost)"` // 客户侧成本合计(USD,源自 bil_usage_logs.total_cost) AccountCost decimal.Decimal `json:"account_cost" orm:"account_cost" description:"上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析)"` // 上游账户成本合计(USD,源自 bil_usage_logs.account_cost,用于利润分析) - CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` // - UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` // SumLatencyMs int64 `json:"sum_latency_ms" orm:"sum_latency_ms" description:"总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值)"` // 总延迟合计(ms,源自 bil_usage_logs.latency_ms;视图按 SUM/COUNT 求均值) SumFirstTokenMs int64 `json:"sum_first_token_ms" orm:"sum_first_token_ms" description:"首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值)"` // 首Token延迟合计(ms,源自 bil_usage_logs.first_token_ms;视图按 SUM/COUNT 求均值) + CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` // + UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` // } diff --git a/internal/model/entity/mdl_tenant_models.go b/internal/model/entity/mdl_tenant_models.go index 4ef62d5..ca7b682 100644 --- a/internal/model/entity/mdl_tenant_models.go +++ b/internal/model/entity/mdl_tenant_models.go @@ -17,7 +17,7 @@ type MdlTenantModels struct { Enabled bool `json:"enabled" orm:"enabled" description:"是否启用(禁用后该租户无法调用此模型)"` // 是否启用(禁用后该租户无法调用此模型) CustomInputPrice *decimal.Decimal `json:"custom_input_price" orm:"custom_input_price" description:"租户自定义输入价格(NULL 表示使用默认定价)"` // 租户自定义输入价格(NULL 表示使用默认定价) CustomOutputPrice *decimal.Decimal `json:"custom_output_price" orm:"custom_output_price" description:"租户自定义输出价格(NULL 表示使用默认定价)"` // 租户自定义输出价格(NULL 表示使用默认定价) - Multiplier decimal.Decimal `json:"multiplier" orm:"multiplier" description:"租户价格倍率(VIP 折扣)。作为 tenant_multiplier 参与最终价格计算:最终价格 = 基础价格 × 租户倍率。倍率来源优先级:discount_ratio > multiplier > 租户等级 price_multiplier"` // 租户价格倍率(VIP 折扣)。作为 tenant_multiplier 参与最终价格计算:最终价格 = 基础价格 × 租户倍率。倍率来源优先级:discount_ratio > multiplier > 租户等级 price_multiplier + Multiplier decimal.Decimal `json:"multiplier" orm:"multiplier" description:"租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)"` // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率) CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 BillingMode string `json:"billing_mode" orm:"billing_mode" description:"覆盖模型计费方式(NULL表示跟随模型默认)"` // 覆盖模型计费方式(NULL表示跟随模型默认) diff --git a/internal/service/tenant.go b/internal/service/tenant.go index 41e6ee1..5d8f969 100644 --- a/internal/service/tenant.go +++ b/internal/service/tenant.go @@ -125,6 +125,10 @@ type ( RequestClosure(ctx context.Context, req *v1.TenantRequestClosureReq) (*v1.TenantRequestClosureRes, error) // CancelClosure 取消关户(仅 owner) CancelClosure(ctx context.Context, req *v1.TenantCancelClosureReq) (*v1.TenantCancelClosureRes, error) + // GetModelList 获取模型广场列表(从默认模型分组加载) + GetModelList(ctx context.Context, req *v1.MarketplaceListReq) (*v1.MarketplaceListRes, error) + // GetModelDetail 获取模型详情 + GetModelDetail(ctx context.Context, req *v1.MarketplaceDetailReq) (*v1.MarketplaceDetailRes, error) // ListMembers returns a paginated list of tenant members. ListMembers(ctx context.Context, req *v1.TenantMemberListReq) (*v1.TenantMemberListRes, error) // InviteMember generates an invitation link. diff --git a/web/tenant/src/api/marketplace.ts b/web/tenant/src/api/marketplace.ts new file mode 100644 index 0000000..69ae282 --- /dev/null +++ b/web/tenant/src/api/marketplace.ts @@ -0,0 +1,36 @@ +import request from '@/utils/request' + +export interface MarketplaceModel { + model_id: string + model_name: string + category: string + description: string + max_context_tokens: number + max_output_tokens: number + input_price: number + output_price: number + tags: string[] + capabilities: Record +} + +export interface MarketplaceListParams { + keyword?: string + category?: string + page: number + page_size: number +} + +export interface MarketplaceListResponse { + list: MarketplaceModel[] + total: number + page: number + page_size: number +} + +export const getMarketplaceModels = (params: MarketplaceListParams) => { + return request.get<{ data: MarketplaceListResponse }>('/tenant/marketplace/models', { params }).then(res => res.data.data) +} + +export const getMarketplaceModelDetail = (modelId: string) => { + return request.get<{ data: MarketplaceModel }>(`/tenant/marketplace/models/${modelId}`).then(res => res.data.data) +} diff --git a/web/tenant/src/router/tenant.ts b/web/tenant/src/router/tenant.ts index b4ac0f5..7a4e08a 100644 --- a/web/tenant/src/router/tenant.ts +++ b/web/tenant/src/router/tenant.ts @@ -40,6 +40,12 @@ const tenantRoutes: RouteRecordRaw[] = [ component: () => import('@/views/auth/JoinPage.vue'), meta: { requiresAuth: false, title: '加入组织' }, }, + { + path: '/marketplace', + name: 'Marketplace', + component: () => import('@/views/marketplace/MarketplacePage.vue'), + meta: { requiresAuth: false, title: '模型广场' }, + }, { path: '/tenant', component: () => import('@/views/Layout.vue'), diff --git a/web/tenant/src/utils/renderUtils.ts b/web/tenant/src/utils/renderUtils.ts index eea9ab4..99f70b6 100644 --- a/web/tenant/src/utils/renderUtils.ts +++ b/web/tenant/src/utils/renderUtils.ts @@ -43,13 +43,15 @@ export interface FormatMoneyOptions { * 统一金额格式化。默认 USD、6 位精度(与 NUMERIC(20,10) 对齐)。 * currency='CNY' 时用 ¥ 前缀 + 2 位精度(订单/充值层)。 * showSign 用 +/− 前缀(交易流水)。 + * 自动去掉末尾的 0(如 $1.500000 → $1.5)。 */ export function formatMoney(value: unknown, opts: FormatMoneyOptions = {}): string { const { currency = 'USD', precision = 6, showSign = false } = opts const num = Number(value ?? 0) const symbol = currency === 'CNY' ? '¥' : '$' const sign = showSign ? (num > 0 ? '+' : num < 0 ? '-' : '') : '' - return `${sign}${symbol}${Math.abs(num).toFixed(precision)}` + // 先 toFixed 确保精度,再用 Number 去掉末尾的 0 + return `${sign}${symbol}${Number(Math.abs(num).toFixed(precision))}` } /** 统一日期格式化:ISO T 转空格、截断到分钟;空值显示 -- */ diff --git a/web/tenant/src/utils/request.ts b/web/tenant/src/utils/request.ts index 1d65c96..c585d6e 100644 --- a/web/tenant/src/utils/request.ts +++ b/web/tenant/src/utils/request.ts @@ -150,6 +150,8 @@ const PUBLIC_PATHS = [ // 找回密码流程无需登录态:发送验证码 / 重置密码 '/email/send-code', '/email/reset-password', + // 模型广场(公开访问) + '/marketplace/', ] function isPublicPath(url?: string): boolean { @@ -240,6 +242,13 @@ request.interceptors.response.use( async (error) => { const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean } + // 公开接口即使暂时返回 401,也不应触发刷新令牌或把访客踢到登录页。 + // 交给调用页面展示自己的错误状态即可。 + if (error.response?.status === 401 && isPublicPath(originalRequest?.url)) { + showRequestError(error, originalRequest) + return Promise.reject(error) + } + if (error.response?.status !== 401 || originalRequest._retry) { // Extract unified error from response body if (error.response?.data && typeof error.response.data === 'object' && 'code' in error.response.data) { diff --git a/web/tenant/src/views/landing/LandingPage.vue b/web/tenant/src/views/landing/LandingPage.vue index 89ac3e8..589d1d4 100644 --- a/web/tenant/src/views/landing/LandingPage.vue +++ b/web/tenant/src/views/landing/LandingPage.vue @@ -18,6 +18,12 @@ {{ siteName }}
+ + + + + 模型广场 + @@ -532,6 +538,20 @@ const mockChartHeights = [45, 62, 38, 71, 55, 82, 67, 48, 73, 58, 90, 65, 52, 78 box-shadow: 0 4px 14px rgba(76,91,142,0.1), inset 0 1px 0 rgba(255,255,255,0.9); } +/* Nav link — 模型广场链接 */ +.btn-nav-link { + display: flex; align-items: center; gap: 6px; + padding: 8px 16px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.72); + font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; + background: rgba(255,255,255,0.55); color: #64748b; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); + text-decoration: none; +} +.btn-nav-link:hover { + background: rgba(255,255,255,0.9); color: #0d9488; + box-shadow: 0 4px 14px rgba(76,91,142,0.1), inset 0 1px 0 rgba(255,255,255,0.9); +} + /* ================================================ 合并区 — 产品标题 + 场景展示 ================================================ */ diff --git a/web/tenant/src/views/marketplace/MarketplacePage.vue b/web/tenant/src/views/marketplace/MarketplacePage.vue new file mode 100644 index 0000000..23044d5 --- /dev/null +++ b/web/tenant/src/views/marketplace/MarketplacePage.vue @@ -0,0 +1,2632 @@ + + + + +