feat(group):新增模型分组功能

This commit is contained in:
qian committed 2026-05-27 20:52:11 +08:00
1 parent b9d20d6b31
commit 09f1b30db4
37 files changed
+1938 -24

No files matched your search

+10
View File
@@ -109,9 +109,19 @@ type IAdminV1 interface {
PricingList(ctx context.Context, req *v1.PricingListReq) (res *v1.PricingListRes, err error)
PricingGet(ctx context.Context, req *v1.PricingGetReq) (res *v1.PricingGetRes, err error)
PricingSet(ctx context.Context, req *v1.PricingSetReq) (res *v1.PricingSetRes, err error)
ModelOptions(ctx context.Context, req *v1.ModelOptionsReq) (res *v1.ModelOptionsRes, err error)
ModelExport(ctx context.Context, req *v1.ModelExportReq) (res *v1.ModelExportRes, err error)
PricingFetchOfficial(ctx context.Context, req *v1.PricingFetchOfficialReq) (res *v1.PricingFetchOfficialRes, err error)
ModelFetchOfficialInfo(ctx context.Context, req *v1.ModelFetchOfficialInfoReq) (res *v1.ModelFetchOfficialInfoRes, err error)
ModelGroupList(ctx context.Context, req *v1.ModelGroupListReq) (res *v1.ModelGroupListRes, err error)
ModelGroupCreate(ctx context.Context, req *v1.ModelGroupCreateReq) (res *v1.ModelGroupCreateRes, err error)
ModelGroupUpdate(ctx context.Context, req *v1.ModelGroupUpdateReq) (res *v1.ModelGroupUpdateRes, err error)
ModelGroupDelete(ctx context.Context, req *v1.ModelGroupDeleteReq) (res *v1.ModelGroupDeleteRes, err error)
GroupModelsList(ctx context.Context, req *v1.GroupModelsListReq) (res *v1.GroupModelsListRes, err error)
GroupModelsSet(ctx context.Context, req *v1.GroupModelsSetReq) (res *v1.GroupModelsSetRes, err error)
TenantGroupsList(ctx context.Context, req *v1.TenantGroupsListReq) (res *v1.TenantGroupsListRes, err error)
TenantGroupsSet(ctx context.Context, req *v1.TenantGroupsSetReq) (res *v1.TenantGroupsSetRes, err error)
ModelGroupOptions(ctx context.Context, req *v1.ModelGroupOptionsReq) (res *v1.ModelGroupOptionsRes, err error)
MonitorDashboard(ctx context.Context, req *v1.MonitorDashboardReq) (res *v1.MonitorDashboardRes, err error)
MonitorTraffic(ctx context.Context, req *v1.MonitorTrafficReq) (res *v1.MonitorTrafficRes, err error)
MonitorLatency(ctx context.Context, req *v1.MonitorLatencyReq) (res *v1.MonitorLatencyRes, err error)
+20
View File
@@ -145,6 +145,26 @@ type ModelUpdateRes struct{}
// ModelDeleteRes 删除模型响应
type ModelDeleteRes struct{}
// ModelOptionsReq 模型选项列表请求(下拉选择专用,不分页)
type ModelOptionsReq struct {
g.Meta `path:"/models/options" method:"get" mime:"json" tags:"管理后台-模型" summary:"模型选项列表(不分页)"`
Status string `json:"status" in:"query" dc:"状态筛选:active/deprecated/offline"`
Category string `json:"category" in:"query" dc:"模型分类筛选:chat/embedding/image/audio/rerank/video"`
}
// ModelOptionsRes 模型选项列表响应
type ModelOptionsRes struct {
List []ModelOptionItem `json:"list"`
}
// ModelOptionItem 模型选项项(精简字段)
type ModelOptionItem struct {
ID int64 `json:"id"`
ModelId string `json:"model_id"`
ModelName string `json:"model_name"`
Category string `json:"category"`
}
// ModelExportReq 导出模型列表请求
type ModelExportReq struct {
g.Meta `path:"/models/export" method:"get" mime:"json" tags:"管理后台-模型" summary:"导出模型列表"`
+154
View File
@@ -0,0 +1,154 @@
package v1
import "github.com/gogf/gf/v2/frame/g"
// ==================== 分组 CRUD ====================
// ModelGroupListReq 模型分组列表请求
type ModelGroupListReq struct {
g.Meta `path:"/model-groups" method:"get" mime:"json" tags:"管理后台-模型分组" summary:"模型分组列表"`
Page int `json:"page" d:"1" v:"min:1" dc:"页码"`
PageSize int `json:"page_size" d:"20" v:"min:1|max:100" dc:"每页数量"`
Status string `json:"status" dc:"状态筛选:active/disabled"`
Search string `json:"search" dc:"搜索关键词(分组名称或标识)"`
}
// ModelGroupListRes 模型分组列表响应
type ModelGroupListRes struct {
List []ModelGroupItem `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// ModelGroupItem 模型分组信息
type ModelGroupItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
Description string `json:"description"`
Status string `json:"status"`
ModelCount int `json:"model_count"`
TenantCount int `json:"tenant_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// ModelGroupCreateReq 创建模型分组请求
type ModelGroupCreateReq struct {
g.Meta `path:"/model-groups" method:"post" mime:"json" tags:"管理后台-模型分组" summary:"创建模型分组"`
Name string `json:"name" v:"required|length:1,100#请输入分组名称|分组名称长度1-100" dc:"分组名称"`
Code string `json:"code" v:"required|length:1,50#请输入分组标识|分组标识长度1-50" dc:"分组唯一标识"`
Description string `json:"description" dc:"分组描述"`
ModelIds []int64 `json:"model_ids" dc:"初始包含的模型ID列表"`
}
// ModelGroupCreateRes 创建模型分组响应
type ModelGroupCreateRes struct {
ID int64 `json:"id"`
}
// ModelGroupUpdateReq 更新模型分组请求
type ModelGroupUpdateReq struct {
g.Meta `path:"/model-groups/{id}" method:"put" mime:"json" tags:"管理后台-模型分组" summary:"更新模型分组"`
ID int64 `json:"id" in:"path" v:"required" dc:"分组ID"`
Name string `json:"name" dc:"分组名称"`
Description string `json:"description" dc:"分组描述"`
Status string `json:"status" v:"in:active,disabled" dc:"状态:active/disabled"`
}
// ModelGroupUpdateRes 更新模型分组响应
type ModelGroupUpdateRes struct{}
// ModelGroupDeleteReq 删除模型分组请求
type ModelGroupDeleteReq struct {
g.Meta `path:"/model-groups/{id}" method:"delete" mime:"json" tags:"管理后台-模型分组" summary:"删除模型分组"`
ID int64 `json:"id" in:"path" v:"required" dc:"分组ID"`
}
// ModelGroupDeleteRes 删除模型分组响应
type ModelGroupDeleteRes struct{}
// ==================== 分组模型管理 ====================
// GroupModelsListReq 查看分组内模型列表请求
type GroupModelsListReq struct {
g.Meta `path:"/model-groups/{id}/models" method:"get" mime:"json" tags:"管理后台-模型分组" summary:"查看分组内模型列表"`
ID int64 `json:"id" in:"path" v:"required" dc:"分组ID"`
}
// GroupModelsListRes 查看分组内模型列表响应
type GroupModelsListRes struct {
List []GroupModelItem `json:"list"`
}
// GroupModelItem 分组内模型信息
type GroupModelItem struct {
ModelId string `json:"model_id"`
ModelName string `json:"model_name"`
Category string `json:"category"`
Status string `json:"status"`
}
// GroupModelsSetReq 设置分组内模型请求(全量替换)
type GroupModelsSetReq struct {
g.Meta `path:"/model-groups/{id}/models" method:"put" mime:"json" tags:"管理后台-模型分组" summary:"设置分组内模型"`
ID int64 `json:"id" in:"path" v:"required" dc:"分组ID"`
ModelIds []int64 `json:"model_ids" v:"required#请选择模型" dc:"模型ID列表"`
}
// GroupModelsSetRes 设置分组内模型响应
type GroupModelsSetRes struct{}
// ==================== 租户分组管理 ====================
// TenantGroupsListReq 查看租户关联分组请求
type TenantGroupsListReq struct {
g.Meta `path:"/tenants/{tenant_id}/groups" method:"get" mime:"json" tags:"管理后台-模型分组" summary:"查看租户关联分组"`
TenantID int64 `json:"tenant_id" in:"path" v:"required" dc:"租户ID"`
}
// TenantGroupsListRes 查看租户关联分组响应
type TenantGroupsListRes struct {
List []TenantGroupItem `json:"list"`
}
// TenantGroupItem 租户关联的分组信息
type TenantGroupItem struct {
GroupID int64 `json:"group_id"`
Name string `json:"name"`
Code string `json:"code"`
Status string `json:"status"`
ModelCount int `json:"model_count"`
}
// TenantGroupsSetReq 设置租户关联分组请求(全量替换)
type TenantGroupsSetReq struct {
g.Meta `path:"/tenants/{tenant_id}/groups" method:"put" mime:"json" tags:"管理后台-模型分组" summary:"设置租户关联分组"`
TenantID int64 `json:"tenant_id" in:"path" v:"required" dc:"租户ID"`
GroupIds []int64 `json:"group_ids" v:"required#请选择分组" dc:"分组ID列表"`
}
// TenantGroupsSetRes 设置租户关联分组响应
type TenantGroupsSetRes struct{}
// ==================== 分组选项列表 ====================
// ModelGroupOptionsReq 分组选项列表请求(下拉选择专用,不分页)
type ModelGroupOptionsReq struct {
g.Meta `path:"/model-groups/options" method:"get" mime:"json" tags:"管理后台-模型分组" summary:"分组选项列表(不分页)"`
Status string `json:"status" in:"query" dc:"状态筛选:active/disabled"`
}
// ModelGroupOptionsRes 分组选项列表响应
type ModelGroupOptionsRes struct {
List []ModelGroupOptionItem `json:"list"`
}
// ModelGroupOptionItem 分组选项项(精简字段)
type ModelGroupOptionItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
ModelCount int `json:"model_count"`
}
+10
View File
@@ -160,6 +160,11 @@ const (
// Model errors
CodeModelNameExists = 10083
// Model group errors
CodeModelGroupNotFound = 10084
CodeModelGroupCodeExists = 10085
CodeModelGroupHasTenants = 10086
// Demo mode
CodeDemoModeRestricted = 10403
@@ -272,6 +277,11 @@ const (
// Model errors
MsgModelNameExists = "模型名称已存在"
// Model group errors
MsgModelGroupNotFound = "模型分组不存在"
MsgModelGroupCodeExists = "模型分组标识已存在"
MsgModelGroupHasTenants = "该分组下存在关联租户,无法删除"
)
// Demo mode messages
@@ -0,0 +1,12 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) GroupModelsList(ctx context.Context, req *v1.GroupModelsListReq) (res *v1.GroupModelsListRes, err error) {
return service.Admin().ListGroupModels(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) GroupModelsSet(ctx context.Context, req *v1.GroupModelsSetReq) (res *v1.GroupModelsSetRes, err error) {
return service.Admin().SetGroupModels(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) ModelGroupCreate(ctx context.Context, req *v1.ModelGroupCreateReq) (res *v1.ModelGroupCreateRes, err error) {
return service.Admin().CreateModelGroup(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) ModelGroupDelete(ctx context.Context, req *v1.ModelGroupDeleteReq) (res *v1.ModelGroupDeleteRes, err error) {
return service.Admin().DeleteModelGroup(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) ModelGroupList(ctx context.Context, req *v1.ModelGroupListReq) (res *v1.ModelGroupListRes, err error) {
return service.Admin().ListModelGroups(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) ModelGroupOptions(ctx context.Context, req *v1.ModelGroupOptionsReq) (res *v1.ModelGroupOptionsRes, err error) {
return service.Admin().ListModelGroupOptions(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) ModelGroupUpdate(ctx context.Context, req *v1.ModelGroupUpdateReq) (res *v1.ModelGroupUpdateRes, err error) {
return service.Admin().UpdateModelGroup(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) ModelOptions(ctx context.Context, req *v1.ModelOptionsReq) (res *v1.ModelOptionsRes, err error) {
return service.Admin().ListModelOptions(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) TenantGroupsList(ctx context.Context, req *v1.TenantGroupsListReq) (res *v1.TenantGroupsListRes, err error) {
return service.Admin().ListTenantGroups(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) TenantGroupsSet(ctx context.Context, req *v1.TenantGroupsSetReq) (res *v1.TenantGroupsSetRes, err error) {
return service.Admin().SetTenantGroups(ctx, req)
}
+87
View File
@@ -0,0 +1,87 @@
// ==========================================================================
// 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"
)
// MdlGroupModelsDao is the data access object for the table mdl_group_models.
type MdlGroupModelsDao 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 MdlGroupModelsColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// MdlGroupModelsColumns defines and stores column names for the table mdl_group_models.
type MdlGroupModelsColumns struct {
Id string // 主键ID
GroupId string // 分组ID(关联 mdl_model_groups.id)
ModelId string // 模型ID(关联 mdl_models.id)
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
}
// mdlGroupModelsColumns holds the columns for the table mdl_group_models.
var mdlGroupModelsColumns = MdlGroupModelsColumns{
Id: "id",
GroupId: "group_id",
ModelId: "model_id",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
}
// NewMdlGroupModelsDao creates and returns a new DAO object for table data access.
func NewMdlGroupModelsDao(handlers ...gdb.ModelHandler) *MdlGroupModelsDao {
return &MdlGroupModelsDao{
group: "default",
table: "mdl_group_models",
columns: mdlGroupModelsColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *MdlGroupModelsDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *MdlGroupModelsDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *MdlGroupModelsDao) Columns() MdlGroupModelsColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *MdlGroupModelsDao) 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 *MdlGroupModelsDao) 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 *MdlGroupModelsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
+91
View File
@@ -0,0 +1,91 @@
// ==========================================================================
// 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"
)
// MdlModelGroupsDao is the data access object for the table mdl_model_groups.
type MdlModelGroupsDao 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 MdlModelGroupsColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// MdlModelGroupsColumns defines and stores column names for the table mdl_model_groups.
type MdlModelGroupsColumns struct {
Id string // 主键ID
Name string // 分组名称(如"全量模型"、"基础对话")
Code string // 分组唯一标识(如 full_access、basic_chat)
Description string // 分组描述
Status string // 状态:active(启用)/ disabled(禁用)
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
}
// mdlModelGroupsColumns holds the columns for the table mdl_model_groups.
var mdlModelGroupsColumns = MdlModelGroupsColumns{
Id: "id",
Name: "name",
Code: "code",
Description: "description",
Status: "status",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
}
// NewMdlModelGroupsDao creates and returns a new DAO object for table data access.
func NewMdlModelGroupsDao(handlers ...gdb.ModelHandler) *MdlModelGroupsDao {
return &MdlModelGroupsDao{
group: "default",
table: "mdl_model_groups",
columns: mdlModelGroupsColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *MdlModelGroupsDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *MdlModelGroupsDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *MdlModelGroupsDao) Columns() MdlModelGroupsColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *MdlModelGroupsDao) 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 *MdlModelGroupsDao) 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 *MdlModelGroupsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -0,0 +1,87 @@
// ==========================================================================
// 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"
)
// MdlTenantGroupsDao is the data access object for the table mdl_tenant_groups.
type MdlTenantGroupsDao 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 MdlTenantGroupsColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// MdlTenantGroupsColumns defines and stores column names for the table mdl_tenant_groups.
type MdlTenantGroupsColumns struct {
Id string // 主键ID
TenantId string // 租户ID(关联 tnt_tenants.id)
GroupId string // 分组ID(关联 mdl_model_groups.id)
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
}
// mdlTenantGroupsColumns holds the columns for the table mdl_tenant_groups.
var mdlTenantGroupsColumns = MdlTenantGroupsColumns{
Id: "id",
TenantId: "tenant_id",
GroupId: "group_id",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
}
// NewMdlTenantGroupsDao creates and returns a new DAO object for table data access.
func NewMdlTenantGroupsDao(handlers ...gdb.ModelHandler) *MdlTenantGroupsDao {
return &MdlTenantGroupsDao{
group: "default",
table: "mdl_tenant_groups",
columns: mdlTenantGroupsColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *MdlTenantGroupsDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *MdlTenantGroupsDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *MdlTenantGroupsDao) Columns() MdlTenantGroupsColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *MdlTenantGroupsDao) 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 *MdlTenantGroupsDao) 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 *MdlTenantGroupsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
+22
View File
@@ -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"
)
// mdlGroupModelsDao is the data access object for the table mdl_group_models.
// You can define custom methods on it to extend its functionality as needed.
type mdlGroupModelsDao struct {
*internal.MdlGroupModelsDao
}
var (
// MdlGroupModels is a globally accessible object for table mdl_group_models operations.
MdlGroupModels = mdlGroupModelsDao{internal.NewMdlGroupModelsDao()}
)
// Add your custom methods and functionality below.
+22
View File
@@ -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"
)
// mdlModelGroupsDao is the data access object for the table mdl_model_groups.
// You can define custom methods on it to extend its functionality as needed.
type mdlModelGroupsDao struct {
*internal.MdlModelGroupsDao
}
var (
// MdlModelGroups is a globally accessible object for table mdl_model_groups operations.
MdlModelGroups = mdlModelGroupsDao{internal.NewMdlModelGroupsDao()}
)
// Add your custom methods and functionality below.
+22
View File
@@ -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"
)
// mdlTenantGroupsDao is the data access object for the table mdl_tenant_groups.
// You can define custom methods on it to extend its functionality as needed.
type mdlTenantGroupsDao struct {
*internal.MdlTenantGroupsDao
}
var (
// MdlTenantGroups is a globally accessible object for table mdl_tenant_groups operations.
MdlTenantGroups = mdlTenantGroupsDao{internal.NewMdlTenantGroupsDao()}
)
// Add your custom methods and functionality below.
+52 -1
View File
@@ -18,6 +18,46 @@ import (
"github.com/qianfree/team-api/internal/utility/export"
)
// ListModelOptions 获取模型选项列表(不分页,用于下拉选择)
func (s *sAdmin) ListModelOptions(ctx context.Context, req *v1.ModelOptionsReq) (*v1.ModelOptionsRes, error) {
query := dao.MdlModels.Ctx(ctx)
if req.Status != "" {
query = query.Where("status", req.Status)
} else {
query = query.Where("status", "active")
}
if req.Category != "" {
query = query.Where("category", req.Category)
}
var models []struct {
ID int64 `json:"id"`
ModelId string `json:"model_id"`
ModelName string `json:"model_name"`
Category string `json:"category"`
}
err := query.Fields("id, model_id, model_name, category").
OrderAsc("category").OrderAsc("model_id").
Scan(&models)
if err != nil {
return nil, err
}
list := make([]v1.ModelOptionItem, 0, len(models))
for _, m := range models {
list = append(list, v1.ModelOptionItem{
ID: m.ID,
ModelId: m.ModelId,
ModelName: m.ModelName,
Category: m.Category,
})
}
return &v1.ModelOptionsRes{List: list}, nil
}
// ListModels 获取模型列表
func (s *sAdmin) ListModels(ctx context.Context, req *v1.ModelListReq) (*v1.ModelListRes, error) {
query := dao.MdlModels.Ctx(ctx)
@@ -252,10 +292,21 @@ func (s *sAdmin) UpdateModel(ctx context.Context, req *v1.ModelUpdateReq) (*v1.M
return nil, nil
}
// DeleteModel 删除模型(同时删除定价记录和租户分配记录)
// DeleteModel 删除模型(同时删除定价记录、租户分配记录和分组关联)
func (s *sAdmin) DeleteModel(ctx context.Context, req *v1.ModelDeleteReq) (*v1.ModelDeleteRes, error) {
_, _ = dao.MdlPricing.Ctx(ctx).Where("model_id", req.ID).Delete()
_, _ = dao.MdlTenantModels.Ctx(ctx).Where("model_id", req.ID).Delete()
// 级联删除:从所有分组中移除该模型,并清除受影响租户的缓存
var affectedGroups []struct {
GroupId int64 `json:"group_id"`
}
dao.MdlGroupModels.Ctx(ctx).Where("model_id", req.ID).Fields("group_id").Scan(&affectedGroups)
_, _ = dao.MdlGroupModels.Ctx(ctx).Where("model_id", req.ID).Delete()
for _, ag := range affectedGroups {
invalidateTenantsInGroup(ctx, ag.GroupId)
}
_, err := dao.MdlModels.Ctx(ctx).Where("id", req.ID).Delete()
if err != nil {
return nil, err
+356
View File
@@ -0,0 +1,356 @@
package admin
import (
"context"
"fmt"
"github.com/gogf/gf/v2/frame/g"
v1 "github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/consts"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/billing"
"github.com/qianfree/team-api/internal/logic/common"
do "github.com/qianfree/team-api/internal/model/do"
)
// ==================== 分组 CRUD ====================
// ListModelGroups 模型分组列表
func (s *sAdmin) ListModelGroups(ctx context.Context, req *v1.ModelGroupListReq) (*v1.ModelGroupListRes, error) {
query := g.DB().Model("mdl_model_groups mg").Ctx(ctx).
LeftJoin("(SELECT group_id, COUNT(*) AS model_count FROM mdl_group_models GROUP BY group_id) mc ON mc.group_id = mg.id").
LeftJoin("(SELECT group_id, COUNT(*) AS tenant_count FROM mdl_tenant_groups GROUP BY group_id) tc ON tc.group_id = mg.id")
if req.Status != "" {
query = query.Where("mg.status", req.Status)
}
if req.Search != "" {
query = query.Where("mg.name LIKE ? OR mg.code LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%")
}
countQuery := g.DB().Model("mdl_model_groups mg").Ctx(ctx)
if req.Status != "" {
countQuery = countQuery.Where("mg.status", req.Status)
}
if req.Search != "" {
countQuery = countQuery.Where("mg.name LIKE ? OR mg.code LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%")
}
total, _ := countQuery.Count()
var results []struct {
ID int64 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
Description string `json:"description"`
Status string `json:"status"`
ModelCount int `json:"model_count"`
TenantCount int `json:"tenant_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
err := query.
Fields("mg.id, mg.name, mg.code, mg.description, mg.status, COALESCE(mc.model_count, 0) AS model_count, COALESCE(tc.tenant_count, 0) AS tenant_count, mg.created_at, mg.updated_at").
OrderAsc("mg.id").
Page(req.Page, req.PageSize).
Scan(&results)
if err != nil {
return nil, err
}
list := make([]v1.ModelGroupItem, 0, len(results))
for _, r := range results {
list = append(list, v1.ModelGroupItem{
ID: r.ID,
Name: r.Name,
Code: r.Code,
Description: r.Description,
Status: r.Status,
ModelCount: r.ModelCount,
TenantCount: r.TenantCount,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
})
}
return &v1.ModelGroupListRes{
List: list,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// CreateModelGroup 创建模型分组
func (s *sAdmin) CreateModelGroup(ctx context.Context, req *v1.ModelGroupCreateReq) (*v1.ModelGroupCreateRes, error) {
count, _ := dao.MdlModelGroups.Ctx(ctx).Where("code", req.Code).Count()
if count > 0 {
return nil, common.NewBusinessError(consts.CodeModelGroupCodeExists, consts.MsgModelGroupCodeExists)
}
result, err := dao.MdlModelGroups.Ctx(ctx).Insert(do.MdlModelGroups{
Name: req.Name,
Code: req.Code,
Description: req.Description,
})
if err != nil {
return nil, err
}
id, _ := result.LastInsertId()
if len(req.ModelIds) > 0 {
for _, modelID := range req.ModelIds {
_, _ = dao.MdlGroupModels.Ctx(ctx).Insert(do.MdlGroupModels{
GroupId: id,
ModelId: modelID,
})
}
}
return &v1.ModelGroupCreateRes{ID: id}, nil
}
// UpdateModelGroup 更新模型分组
func (s *sAdmin) UpdateModelGroup(ctx context.Context, req *v1.ModelGroupUpdateReq) (*v1.ModelGroupUpdateRes, error) {
count, _ := dao.MdlModelGroups.Ctx(ctx).Where("id", req.ID).Count()
if count == 0 {
return nil, common.NewBusinessError(consts.CodeModelGroupNotFound, consts.MsgModelGroupNotFound)
}
data := do.MdlModelGroups{}
if req.Name != "" {
data.Name = req.Name
}
if req.Description != "" {
data.Description = req.Description
}
if req.Status != "" {
data.Status = req.Status
}
_, err := dao.MdlModelGroups.Ctx(ctx).Where("id", req.ID).Data(data).Update()
if err != nil {
return nil, err
}
if req.Status != "" {
invalidateTenantsInGroup(ctx, req.ID)
}
return nil, nil
}
// DeleteModelGroup 删除模型分组
func (s *sAdmin) DeleteModelGroup(ctx context.Context, req *v1.ModelGroupDeleteReq) (*v1.ModelGroupDeleteRes, error) {
count, _ := dao.MdlModelGroups.Ctx(ctx).Where("id", req.ID).Count()
if count == 0 {
return nil, common.NewBusinessError(consts.CodeModelGroupNotFound, consts.MsgModelGroupNotFound)
}
tenantCount, _ := dao.MdlTenantGroups.Ctx(ctx).Where("group_id", req.ID).Count()
if tenantCount > 0 {
return nil, common.NewBusinessError(consts.CodeModelGroupHasTenants, consts.MsgModelGroupHasTenants)
}
_, _ = dao.MdlGroupModels.Ctx(ctx).Where("group_id", req.ID).Delete()
_, err := dao.MdlModelGroups.Ctx(ctx).Where("id", req.ID).Delete()
if err != nil {
return nil, err
}
return nil, nil
}
// ListModelGroupOptions 分组选项列表(不分页,用于下拉选择)
func (s *sAdmin) ListModelGroupOptions(ctx context.Context, req *v1.ModelGroupOptionsReq) (*v1.ModelGroupOptionsRes, error) {
query := g.DB().Model("mdl_model_groups mg").Ctx(ctx).
LeftJoin("(SELECT group_id, COUNT(*) AS model_count FROM mdl_group_models GROUP BY group_id) mc ON mc.group_id = mg.id")
if req.Status != "" {
query = query.Where("mg.status", req.Status)
} else {
query = query.Where("mg.status", "active")
}
var results []struct {
ID int64 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
ModelCount int `json:"model_count"`
}
err := query.
Fields("mg.id, mg.name, mg.code, COALESCE(mc.model_count, 0) AS model_count").
OrderAsc("mg.id").
Scan(&results)
if err != nil {
return nil, err
}
list := make([]v1.ModelGroupOptionItem, 0, len(results))
for _, r := range results {
list = append(list, v1.ModelGroupOptionItem{
ID: r.ID,
Name: r.Name,
Code: r.Code,
ModelCount: r.ModelCount,
})
}
return &v1.ModelGroupOptionsRes{List: list}, nil
}
// ==================== 分组模型管理 ====================
// ListGroupModels 查看分组内模型列表
func (s *sAdmin) ListGroupModels(ctx context.Context, req *v1.GroupModelsListReq) (*v1.GroupModelsListRes, error) {
var results []struct {
ModelId string `json:"model_id"`
ModelName string `json:"model_name"`
Category string `json:"category"`
Status string `json:"status"`
}
err := dao.MdlGroupModels.Ctx(ctx).As("gm").
InnerJoin("mdl_models m ON gm.model_id = m.id").
Where("gm.group_id", req.ID).
Fields("m.model_id, m.model_name, m.category, m.status").
OrderAsc("m.category").
OrderAsc("m.model_id").
Scan(&results)
if err != nil {
return nil, err
}
list := make([]v1.GroupModelItem, 0, len(results))
for _, r := range results {
list = append(list, v1.GroupModelItem{
ModelId: r.ModelId,
ModelName: r.ModelName,
Category: r.Category,
Status: r.Status,
})
}
return &v1.GroupModelsListRes{List: list}, nil
}
// SetGroupModels 设置分组内模型(全量替换)
func (s *sAdmin) SetGroupModels(ctx context.Context, req *v1.GroupModelsSetReq) (*v1.GroupModelsSetRes, error) {
count, _ := dao.MdlModelGroups.Ctx(ctx).Where("id", req.ID).Count()
if count == 0 {
return nil, common.NewBusinessError(consts.CodeModelGroupNotFound, consts.MsgModelGroupNotFound)
}
// 删除旧关联
_, err := dao.MdlGroupModels.Ctx(ctx).Where("group_id", req.ID).Delete()
if err != nil {
return nil, err
}
// 批量插入新关联
if len(req.ModelIds) > 0 {
insertData := make([]do.MdlGroupModels, 0, len(req.ModelIds))
for _, modelID := range req.ModelIds {
insertData = append(insertData, do.MdlGroupModels{
GroupId: req.ID,
ModelId: modelID,
})
}
_, err = dao.MdlGroupModels.Ctx(ctx).Batch(len(insertData)).Insert(insertData)
if err != nil {
return nil, err
}
}
invalidateTenantsInGroup(ctx, req.ID)
return nil, nil
}
// ==================== 租户分组管理 ====================
// ListTenantGroups 查看租户关联的分组列表
func (s *sAdmin) ListTenantGroups(ctx context.Context, req *v1.TenantGroupsListReq) (*v1.TenantGroupsListRes, error) {
var results []struct {
GroupID int64 `json:"group_id"`
Name string `json:"name"`
Code string `json:"code"`
Status string `json:"status"`
ModelCount int `json:"model_count"`
}
err := dao.MdlTenantGroups.Ctx(ctx).As("tg").
InnerJoin("mdl_model_groups g ON tg.group_id = g.id").
LeftJoin("(SELECT group_id, COUNT(*) AS model_count FROM mdl_group_models GROUP BY group_id) mc ON mc.group_id = g.id").
Where("tg.tenant_id", req.TenantID).
Fields("g.id AS group_id, g.name, g.code, g.status, COALESCE(mc.model_count, 0) AS model_count").
OrderAsc("g.id").
Scan(&results)
if err != nil {
return nil, err
}
list := make([]v1.TenantGroupItem, 0, len(results))
for _, r := range results {
list = append(list, v1.TenantGroupItem{
GroupID: r.GroupID,
Name: r.Name,
Code: r.Code,
Status: r.Status,
ModelCount: r.ModelCount,
})
}
return &v1.TenantGroupsListRes{List: list}, nil
}
// SetTenantGroups 设置租户关联的分组(全量替换)
func (s *sAdmin) SetTenantGroups(ctx context.Context, req *v1.TenantGroupsSetReq) (*v1.TenantGroupsSetRes, error) {
// 删除旧关联
_, err := dao.MdlTenantGroups.Ctx(ctx).Where("tenant_id", req.TenantID).Delete()
if err != nil {
return nil, err
}
// 批量插入新关联
if len(req.GroupIds) > 0 {
insertData := make([]do.MdlTenantGroups, 0, len(req.GroupIds))
for _, groupID := range req.GroupIds {
insertData = append(insertData, do.MdlTenantGroups{
TenantId: req.TenantID,
GroupId: groupID,
})
}
_, err = dao.MdlTenantGroups.Ctx(ctx).Batch(len(insertData)).Insert(insertData)
if err != nil {
return nil, err
}
}
invalidateTenantGroupCache(ctx, req.TenantID)
billing.ClearTenantPriceCache(ctx, req.TenantID)
return nil, nil
}
// ==================== 缓存失效辅助函数 ====================
func invalidateTenantGroupCache(ctx context.Context, tenantID int64) {
common.TenantGroupModelCache.Delete(ctx, fmt.Sprintf("%d", tenantID))
}
func invalidateTenantsInGroup(ctx context.Context, groupID int64) {
var tenants []struct {
TenantId int64 `json:"tenant_id"`
}
dao.MdlTenantGroups.Ctx(ctx).
Where("group_id", groupID).
Fields("tenant_id").
Scan(&tenants)
for _, t := range tenants {
invalidateTenantGroupCache(ctx, t.TenantId)
}
}
+3
View File
@@ -192,3 +192,6 @@ func (c *Cache) DeleteByPattern(ctx context.Context, pattern string) {
func PublishInvalidation(ctx context.Context, fullKey string) {
_, _ = g.Redis().Do(ctx, "PUBLISH", "cache:invalidate", fullKey)
}
// TenantGroupModelCache 缓存租户通过分组可访问的模型集合,TTL 300s
var TenantGroupModelCache = NewCache("tenant_group_models", 300*time.Second)
+148 -18
View File
@@ -24,6 +24,9 @@ import (
// modelCache 模型信息缓存(TTL 600s)
var modelCache = lcommon.NewCache("model", 600*time.Second)
// tenantGroupModelCache 租户分组模型集合缓存(通过 lcommon.TenantGroupModelCache 共享)
// 实际缓存实例在 lcommon 包中,admin 和 relay 共用
// DataProviderImpl DataProvider 接口的 GoFrame ORM 实现
type DataProviderImpl struct{}
@@ -230,9 +233,9 @@ func (p *DataProviderImpl) CheckTenantModelAccess(ctx context.Context, tenantID
return false, nil, err
}
// 如果没有分配记录,默认允许(向后兼容:未分配模型时所有活跃模型可用)
// 如果没有分配记录,检查分组访问权限
if row == nil {
return true, nil, nil
return checkGroupModelAccess(ctx, tenantID, modelName)
}
if !row.Enabled && row.ChannelScope == "" {
return false, nil, nil
@@ -247,6 +250,110 @@ func (p *DataProviderImpl) CheckTenantModelAccess(ctx context.Context, tenantID
return row.Enabled, scope, nil
}
// checkGroupModelAccess 检查租户是否通过分组获得模型访问权限
func checkGroupModelAccess(ctx context.Context, tenantID int64, modelName string) (bool, []int64, error) {
cacheKey := fmt.Sprintf("%d", tenantID)
// 尝试从缓存获取
cachedSet, found := lcommon.TenantGroupModelCache.Get(ctx, cacheKey)
if found {
if cachedSet == nil {
// nil 表示无分组 → 向后兼容,允许所有模型
return true, nil, nil
}
if modelSet, ok := cachedSet.(map[string]bool); ok {
_, inGroup := modelSet[modelName]
return inGroup, nil, nil
}
}
// 缓存未命中:查询数据库
groupCount, _ := dao.MdlTenantGroups.Ctx(ctx).
Where("tenant_id", tenantID).
Count()
if groupCount == 0 {
// 无分组 → 向后兼容,允许所有模型
lcommon.TenantGroupModelCache.Set(ctx, cacheKey, nil)
return true, nil, nil
}
// 查询租户所有活跃分组中的可用模型
modelSet, err := queryTenantGroupModels(ctx, tenantID)
if err != nil {
return false, nil, err
}
// 缓存结果
lcommon.TenantGroupModelCache.Set(ctx, cacheKey, modelSet)
_, inGroup := modelSet[modelName]
return inGroup, nil, nil
}
// queryTenantGroupModels 查询租户通过分组可访问的所有模型
func queryTenantGroupModels(ctx context.Context, tenantID int64) (map[string]bool, error) {
var models []struct {
ModelId string `json:"model_id"`
}
err := g.DB().Model("mdl_group_models gm").Ctx(ctx).
InnerJoin("mdl_model_groups g ON gm.group_id = g.id").
InnerJoin("mdl_models m ON gm.model_id = m.id").
InnerJoin("mdl_tenant_groups tg ON tg.group_id = g.id").
Where("tg.tenant_id", tenantID).
Where("g.status", "active").
Where("m.status", "active").
Fields("m.model_id").
Scan(&models)
if err != nil {
return nil, err
}
result := make(map[string]bool, len(models))
for _, m := range models {
result[m.ModelId] = true
}
return result, nil
}
// GetTenantGroupModelIDs 获取租户通过分组可访问的模型内部ID集合(用于 GetAvailableModels)
func GetTenantGroupModelIDs(ctx context.Context, tenantID int64) map[int64]bool {
cacheKey := fmt.Sprintf("%d", tenantID)
// 尝试从缓存获取 model_id string 集合
cachedSet, found := lcommon.TenantGroupModelCache.Get(ctx, cacheKey)
if found {
if cachedSet == nil {
return nil
}
// 缓存中存的是 map[string]bool,需要转换为 map[int64]bool
// 这里直接查数据库获取 int64 版本
}
// 查询模型内部 ID
var models []struct {
Id int64 `json:"id"`
}
err := g.DB().Model("mdl_group_models gm").Ctx(ctx).
InnerJoin("mdl_model_groups g ON gm.group_id = g.id").
InnerJoin("mdl_models m ON gm.model_id = m.id").
InnerJoin("mdl_tenant_groups tg ON tg.group_id = g.id").
Where("tg.tenant_id", tenantID).
Where("g.status", "active").
Where("m.status", "active").
Fields("m.id").
Scan(&models)
if err != nil || len(models) == 0 {
return nil
}
result := make(map[int64]bool, len(models))
for _, m := range models {
result[m.Id] = true
}
return result
}
// GetModelMapping 实现 DataProvider.GetModelMapping
func (p *DataProviderImpl) GetModelMapping(ctx context.Context, modelName string) (string, string, error) {
cacheKey := modelName
@@ -623,34 +730,57 @@ func (p *DataProviderImpl) GetAvailableModels(ctx context.Context, tenantID int6
var models []modelRow
var err error
fields := "m.model_id, m.model_name, m.category, m.status, m.max_context_tokens, m.max_output_tokens, m.capabilities"
fieldsNoAlias := "model_id, model_name, category, status, max_context_tokens, max_output_tokens, capabilities"
if tenantID > 0 {
// 检查租户是否有模型分配记录
count, _ := dao.MdlTenantModels.Ctx(ctx).
// 获取显式分配的模型
tenantModelCount, _ := dao.MdlTenantModels.Ctx(ctx).
Where("tenant_id", tenantID).
Count()
if count > 0 {
// 有分配记录:只返回租户启用的模型
err = g.DB().Model("mdl_models m").Ctx(ctx).
InnerJoin("mdl_tenant_models tm ON tm.model_id = m.id").
Where("tm.tenant_id", tenantID).
Where("tm.enabled", true).
Where("m.status", "active").
Fields(fields).
OrderAsc("m.category").
OrderAsc("m.model_id").
Scan(&models)
} else {
// 无分配记录:返回所有活跃模型(向后兼容)
// 获取分组分配的模型
groupModelIDs := GetTenantGroupModelIDs(ctx, tenantID)
hasExplicit := tenantModelCount > 0
hasGroup := len(groupModelIDs) > 0
if !hasExplicit && !hasGroup {
// 无分配记录且无分组:返回所有活跃模型(向后兼容)
err = dao.MdlModels.Ctx(ctx).
Where("status", "active").
Fields(fieldsNoAlias).
OrderAsc("category").
OrderAsc("model_id").
Scan(&models)
} else {
// 构建合并的模型 ID 集合
allModelIDs := make([]int64, 0)
if hasExplicit {
var explicitIDs []struct {
ModelID int64 `json:"model_id"`
}
dao.MdlTenantModels.Ctx(ctx).
Where("tenant_id", tenantID).Where("enabled", true).
Fields("model_id").Scan(&explicitIDs)
for _, id := range explicitIDs {
allModelIDs = append(allModelIDs, id.ModelID)
}
}
for id := range groupModelIDs {
allModelIDs = append(allModelIDs, id)
}
if len(allModelIDs) > 0 {
err = dao.MdlModels.Ctx(ctx).
WhereIn("id", allModelIDs).
Where("status", "active").
Fields(fieldsNoAlias).
OrderAsc("category").
OrderAsc("model_id").
Scan(&models)
}
}
} else {
// tenantID == 0:返回所有活跃模型(公开端点场景)
+73
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"github.com/gogf/gf/v2/frame/g"
v1 "github.com/qianfree/team-api/api/tenant/v1"
"github.com/qianfree/team-api/internal/dao"
)
@@ -183,6 +184,78 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
list = append(list, item)
}
// 追加分组来源的模型(不在显式分配中的模型)
explicitModelIDs := make(map[int64]bool, len(results))
for _, r := range results {
explicitModelIDs[r.ModelDBID] = true
}
var groupModels []struct {
ModelDBID int64 `json:"model_db_id"`
ModelId string `json:"model_id"`
ModelName string `json:"model_name"`
Category string `json:"category"`
MaxContextTokens int `json:"max_context_tokens"`
MaxOutputTokens int `json:"max_output_tokens"`
Description string `json:"description"`
Tags string `json:"tags"`
Capabilities string `json:"capabilities"`
BaseBillingMode string `json:"base_billing_mode"`
BaseInputPrice float64 `json:"base_input_price"`
BaseOutputPrice float64 `json:"base_output_price"`
}
groupQuery := g.DB().Model("mdl_group_models gm").Ctx(ctx).
InnerJoin("mdl_model_groups g ON gm.group_id = g.id").
InnerJoin("mdl_models m ON gm.model_id = m.id").
InnerJoin("mdl_tenant_groups tg ON tg.group_id = g.id").
LeftJoin("mdl_pricing p ON p.model_id = m.id AND p.min_tokens = 0").
Where("tg.tenant_id", tenantID).
Where("g.status", "active").
Where("m.status", "active")
if len(explicitModelIDs) > 0 {
ids := make([]int64, 0, len(explicitModelIDs))
for id := range explicitModelIDs {
ids = append(ids, id)
}
groupQuery = groupQuery.WhereNotIn("m.id", ids)
}
if req.Category != "" {
groupQuery = groupQuery.Where("m.category", req.Category)
}
if req.Search != "" {
groupQuery = groupQuery.Where("m.model_id LIKE ? OR m.model_name LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%")
}
_ = groupQuery.
Fields("DISTINCT m.id AS model_db_id, m.model_id, m.model_name, m.category, m.max_context_tokens, m.max_output_tokens, m.description, m.tags, m.capabilities, p.billing_mode AS base_billing_mode, p.input_price AS base_input_price, p.output_price AS base_output_price").
OrderAsc("m.category").
OrderAsc("m.model_id").
Scan(&groupModels)
for _, gm := range groupModels {
billingMode := gm.BaseBillingMode
if billingMode == "" {
billingMode = "token"
}
item := v1.TenantAvailableModelItem{
ModelId: gm.ModelId,
ModelName: gm.ModelName,
Category: gm.Category,
MaxContext: gm.MaxContextTokens,
MaxOutput: gm.MaxOutputTokens,
Description: gm.Description,
Tags: gm.Tags,
Capabilities: gm.Capabilities,
BillingMode: &billingMode,
InputPrice: &gm.BaseInputPrice,
OutputPrice: &gm.BaseOutputPrice,
}
list = append(list, item)
}
return &v1.TenantAvailableModelsRes{List: list}, nil
}
+20
View File
@@ -0,0 +1,20 @@
// =================================================================================
// 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"
)
// MdlGroupModels is the golang structure of table mdl_group_models for DAO operations like Where/Data.
type MdlGroupModels struct {
g.Meta `orm:"table:mdl_group_models, do:true"`
Id any // 主键ID
GroupId any // 分组ID(关联 mdl_model_groups.id)
ModelId any // 模型ID(关联 mdl_models.id)
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
}
+22
View File
@@ -0,0 +1,22 @@
// =================================================================================
// 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"
)
// MdlModelGroups is the golang structure of table mdl_model_groups for DAO operations like Where/Data.
type MdlModelGroups struct {
g.Meta `orm:"table:mdl_model_groups, do:true"`
Id any // 主键ID
Name any // 分组名称(如"全量模型"、"基础对话")
Code any // 分组唯一标识(如 full_access、basic_chat)
Description any // 分组描述
Status any // 状态:active(启用)/ disabled(禁用)
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
}
+20
View File
@@ -0,0 +1,20 @@
// =================================================================================
// 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"
)
// MdlTenantGroups is the golang structure of table mdl_tenant_groups for DAO operations like Where/Data.
type MdlTenantGroups struct {
g.Meta `orm:"table:mdl_tenant_groups, do:true"`
Id any // 主键ID
TenantId any // 租户ID(关联 tnt_tenants.id)
GroupId any // 分组ID(关联 mdl_model_groups.id)
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
}
+18
View File
@@ -0,0 +1,18 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// MdlGroupModels is the golang structure for table mdl_group_models.
type MdlGroupModels struct {
Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID
GroupId int64 `json:"group_id" orm:"group_id" description:"分组ID(关联 mdl_model_groups.id)"` // 分组ID(关联 mdl_model_groups.id)
ModelId int64 `json:"model_id" orm:"model_id" description:"模型ID(关联 mdl_models.id)"` // 模型ID(关联 mdl_models.id)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
}
+20
View File
@@ -0,0 +1,20 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// MdlModelGroups is the golang structure for table mdl_model_groups.
type MdlModelGroups struct {
Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID
Name string `json:"name" orm:"name" description:"分组名称(如\"全量模型\"、\"基础对话\")"` // 分组名称(如"全量模型"、"基础对话")
Code string `json:"code" orm:"code" description:"分组唯一标识(如 full_access、basic_chat)"` // 分组唯一标识(如 full_access、basic_chat)
Description string `json:"description" orm:"description" description:"分组描述"` // 分组描述
Status string `json:"status" orm:"status" description:"状态:active(启用)/ disabled(禁用)"` // 状态:active(启用)/ disabled(禁用)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
}
@@ -0,0 +1,18 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// MdlTenantGroups is the golang structure for table mdl_tenant_groups.
type MdlTenantGroups struct {
Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID(关联 tnt_tenants.id)"` // 租户ID(关联 tnt_tenants.id)
GroupId int64 `json:"group_id" orm:"group_id" description:"分组ID(关联 mdl_model_groups.id)"` // 分组ID(关联 mdl_model_groups.id)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
}
+21 -1
View File
@@ -206,13 +206,15 @@ type (
ResetMemberPassword(ctx context.Context, req *v1.AdminMemberResetPasswordReq) (*v1.AdminMemberResetPasswordRes, error)
// ExportMembers exports member list to CSV or Excel.
ExportMembers(ctx context.Context, req *v1.AdminMemberExportReq) (*v1.AdminMemberExportRes, error)
// ListModelOptions 获取模型选项列表(不分页,用于下拉选择)
ListModelOptions(ctx context.Context, req *v1.ModelOptionsReq) (*v1.ModelOptionsRes, error)
// ListModels 获取模型列表
ListModels(ctx context.Context, req *v1.ModelListReq) (*v1.ModelListRes, error)
// CreateModel 创建模型(自动创建默认 token 定价记录)
CreateModel(ctx context.Context, req *v1.ModelCreateReq) (*v1.ModelCreateRes, error)
// UpdateModel 更新模型(含弃用状态管理)
UpdateModel(ctx context.Context, req *v1.ModelUpdateReq) (*v1.ModelUpdateRes, error)
// DeleteModel 删除模型(同时删除定价记录和租户分配记录)
// DeleteModel 删除模型(同时删除定价记录、租户分配记录和分组关联)
DeleteModel(ctx context.Context, req *v1.ModelDeleteReq) (*v1.ModelDeleteRes, error)
// ListModelPricing 模型定价列表(模型定价页面专用)
ListModelPricing(ctx context.Context, req *v1.PricingListReq) (*v1.PricingListRes, error)
@@ -226,6 +228,24 @@ type (
FetchOfficialModelInfo(ctx context.Context, req *v1.ModelFetchOfficialInfoReq) (*v1.ModelFetchOfficialInfoRes, error)
// ExportModels exports model list to CSV or Excel.
ExportModels(ctx context.Context, req *v1.ModelExportReq) (*v1.ModelExportRes, error)
// ListModelGroups 模型分组列表
ListModelGroups(ctx context.Context, req *v1.ModelGroupListReq) (*v1.ModelGroupListRes, error)
// CreateModelGroup 创建模型分组
CreateModelGroup(ctx context.Context, req *v1.ModelGroupCreateReq) (*v1.ModelGroupCreateRes, error)
// UpdateModelGroup 更新模型分组
UpdateModelGroup(ctx context.Context, req *v1.ModelGroupUpdateReq) (*v1.ModelGroupUpdateRes, error)
// DeleteModelGroup 删除模型分组
DeleteModelGroup(ctx context.Context, req *v1.ModelGroupDeleteReq) (*v1.ModelGroupDeleteRes, error)
// ListModelGroupOptions 分组选项列表(不分页,用于下拉选择)
ListModelGroupOptions(ctx context.Context, req *v1.ModelGroupOptionsReq) (*v1.ModelGroupOptionsRes, error)
// ListGroupModels 查看分组内模型列表
ListGroupModels(ctx context.Context, req *v1.GroupModelsListReq) (*v1.GroupModelsListRes, error)
// SetGroupModels 设置分组内模型(全量替换)
SetGroupModels(ctx context.Context, req *v1.GroupModelsSetReq) (*v1.GroupModelsSetRes, error)
// ListTenantGroups 查看租户关联的分组列表
ListTenantGroups(ctx context.Context, req *v1.TenantGroupsListReq) (*v1.TenantGroupsListRes, error)
// SetTenantGroups 设置租户关联的分组(全量替换)
SetTenantGroups(ctx context.Context, req *v1.TenantGroupsSetReq) (*v1.TenantGroupsSetRes, error)
// ListTemplates 获取通知模板列表(分页)
ListTemplates(ctx context.Context, req *v1.TemplateListReq) (*v1.TemplateListRes, error)
// GetTemplate 获取单个通知模板
+66
View File
@@ -0,0 +1,66 @@
-- +goose Up
-- 模型分组功能:通过分组批量管理租户可用的模型
-- 模型分组定义
CREATE TABLE mdl_model_groups (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'active' NOT NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT uk_mdl_model_groups_code UNIQUE (code)
);
COMMENT ON TABLE mdl_model_groups IS '模型分组定义';
COMMENT ON COLUMN mdl_model_groups.id IS '主键ID';
COMMENT ON COLUMN mdl_model_groups.name IS '分组名称(如"全量模型"、"基础对话")';
COMMENT ON COLUMN mdl_model_groups.code IS '分组唯一标识(如 full_access、basic_chat)';
COMMENT ON COLUMN mdl_model_groups.description IS '分组描述';
COMMENT ON COLUMN mdl_model_groups.status IS '状态:active(启用)/ disabled(禁用)';
COMMENT ON COLUMN mdl_model_groups.created_at IS '创建时间';
COMMENT ON COLUMN mdl_model_groups.updated_at IS '更新时间';
-- 分组-模型关联(多对多)
CREATE TABLE mdl_group_models (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL,
model_id BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT uk_mdl_group_models UNIQUE (group_id, model_id)
);
CREATE INDEX idx_mdl_group_models_model_id ON mdl_group_models (model_id);
COMMENT ON TABLE mdl_group_models IS '分组-模型关联';
COMMENT ON COLUMN mdl_group_models.id IS '主键ID';
COMMENT ON COLUMN mdl_group_models.group_id IS '分组ID(关联 mdl_model_groups.id)';
COMMENT ON COLUMN mdl_group_models.model_id IS '模型ID(关联 mdl_models.id)';
COMMENT ON COLUMN mdl_group_models.created_at IS '创建时间';
COMMENT ON COLUMN mdl_group_models.updated_at IS '更新时间';
-- 租户-分组关联(多对多)
CREATE TABLE mdl_tenant_groups (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
group_id BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT uk_mdl_tenant_groups UNIQUE (tenant_id, group_id)
);
CREATE INDEX idx_mdl_tenant_groups_tenant_id ON mdl_tenant_groups (tenant_id);
COMMENT ON TABLE mdl_tenant_groups IS '租户-分组关联';
COMMENT ON COLUMN mdl_tenant_groups.id IS '主键ID';
COMMENT ON COLUMN mdl_tenant_groups.tenant_id IS '租户ID(关联 tnt_tenants.id)';
COMMENT ON COLUMN mdl_tenant_groups.group_id IS '分组ID(关联 mdl_model_groups.id)';
COMMENT ON COLUMN mdl_tenant_groups.created_at IS '创建时间';
COMMENT ON COLUMN mdl_tenant_groups.updated_at IS '更新时间';
-- +goose Down
DROP TABLE IF EXISTS mdl_tenant_groups;
DROP TABLE IF EXISTS mdl_group_models;
DROP TABLE IF EXISTS mdl_model_groups;
+6
View File
@@ -48,6 +48,12 @@ const adminRoutes: RouteRecordRaw[] = [
component: () => import('@/views/ai/ModelsPage.vue'),
meta: { title: '模型管理' },
},
{
path: 'model-groups',
name: 'AdminModelGroups',
component: () => import('@/views/ai/ModelGroupsPage.vue'),
meta: { title: '模型分组' },
},
{
path: 'channels',
name: 'AdminChannels',
+1
View File
@@ -152,6 +152,7 @@ const menuGroups = [
icon: IconApps,
items: [
{ name: 'AdminModels', label: '模型列表', icon: IconApps },
{ name: 'AdminModelGroups', label: '模型分组', icon: IconStorage },
{ name: 'AdminChannels', label: '渠道管理', icon: IconBranch },
{ name: 'AdminTaskLogs', label: '任务日志', icon: IconCalendar },
{ name: 'AdminModelPricing', label: '模型定价', icon: IconLayers },
+332
View File
@@ -0,0 +1,332 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, h } from 'vue'
import { Tag, Button, Space, Popconfirm, Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import PageHeader from '@/components/PageHeader.vue'
import request from '@/utils/request'
const loading = ref(false)
const data = ref<any[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showPageSize: true,
pageSizeOptions: [10, 20, 50],
})
const filterSearch = ref('')
const filterStatus = ref<string | null>(null)
async function fetchData() {
loading.value = true
try {
const params: any = {
page: pagination.current,
page_size: pagination.pageSize,
}
if (filterSearch.value) params.search = filterSearch.value
if (filterStatus.value) params.status = filterStatus.value
const res: any = await request.get('/admin/model-groups', { params })
data.value = res.data?.data?.list || res.data?.list || []
pagination.total = res.data?.data?.total || res.data?.total || 0
} catch {
} finally {
loading.value = false
}
}
function handlePageChange(page: number) {
pagination.current = page
fetchData()
}
function handlePageSizeChange(pageSize: number) {
pagination.pageSize = pageSize
pagination.current = 1
fetchData()
}
function resetFilter() {
filterSearch.value = ''
filterStatus.value = null
pagination.current = 1
fetchData()
}
const columns: TableColumnData[] = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '分组名称', dataIndex: 'name', width: 160, ellipsis: true },
{ title: '标识', dataIndex: 'code', width: 140, ellipsis: true },
{ title: '描述', dataIndex: 'description', width: 200, ellipsis: true },
{
title: '状态', dataIndex: 'status', width: 80,
render({ record }) {
const color = record.status === 'active' ? 'green' : undefined
const label = record.status === 'active' ? '启用' : '禁用'
return h(Tag, { color, size: 'small' }, () => label)
},
},
{ title: '模型数', dataIndex: 'model_count', width: 80 },
{ title: '租户数', dataIndex: 'tenant_count', width: 80 },
{
title: '创建时间', dataIndex: 'created_at', width: 170,
render({ record }) {
return record.created_at ? new Date(record.created_at).toLocaleString() : '-'
},
},
{
title: '操作', dataIndex: 'actions', width: 220, fixed: 'right',
render({ record }) {
return h(Space, { size: 4 }, () => [
h(Button, { size: 'small', onClick: () => openModelsModal(record) }, () => '管理模型'),
h(Button, { size: 'small', onClick: () => openEdit(record) }, () => '编辑'),
h(Popconfirm, { content: '确定删除该分组?', onOk: () => handleDelete(record) }, () =>
h(Button, { size: 'small', status: 'danger' }, () => '删除')
),
])
},
},
]
// ==================== 分组 CRUD ====================
const showModal = ref(false)
const modalTitle = ref('创建分组')
const formLoading = ref(false)
const editingId = ref<number | null>(null)
const form = reactive({
name: '',
code: '',
description: '',
})
function resetForm() {
form.name = ''
form.code = ''
form.description = ''
}
function openCreate() {
resetForm()
editingId.value = null
modalTitle.value = '创建分组'
showModal.value = true
}
function openEdit(row: any) {
form.name = row.name || ''
form.code = row.code || ''
form.description = row.description || ''
editingId.value = row.id
modalTitle.value = '编辑分组'
showModal.value = true
}
async function handleSubmit(done: () => void) {
if (!form.name.trim()) {
Message.warning('请输入分组名称')
return
}
formLoading.value = true
try {
const payload = { name: form.name, code: form.code, description: form.description }
if (editingId.value) {
await request.put(`/admin/model-groups/${editingId.value}`, payload)
Message.success('更新成功')
} else {
if (!form.code.trim()) {
Message.warning('请输入分组标识')
formLoading.value = false
return
}
await request.post('/admin/model-groups', payload)
Message.success('创建成功')
}
done()
fetchData()
} catch {
return false
} finally {
formLoading.value = false
}
}
async function handleDelete(row: any) {
try {
await request.delete(`/admin/model-groups/${row.id}`)
Message.success('删除成功')
fetchData()
} catch {
}
}
// ==================== 模型管理弹窗 ====================
const showModelsModal = ref(false)
const modelsLoading = ref(false)
const groupModels = ref<any[]>([])
const allModels = ref<any[]>([])
const selectedModelIds = ref<number[]>([])
const currentGroup = ref<any>(null)
async function fetchAllModels() {
try {
const res: any = await request.get('/admin/models/options')
allModels.value = res.data?.data?.list || res.data?.list || []
} catch {
}
}
async function openModelsModal(row: any) {
currentGroup.value = row
selectedModelIds.value = []
showModelsModal.value = true
modelsLoading.value = true
try {
const [groupRes] = await Promise.all([
request.get(`/admin/model-groups/${row.id}/models`),
allModels.value.length === 0 ? fetchAllModels() : Promise.resolve(),
])
groupModels.value = (groupRes as any).data?.data?.list || (groupRes as any).data?.list || []
selectedModelIds.value = groupModels.value.map((m: any) => {
const found = allModels.value.find((am: any) => am.model_id === m.model_id)
return found ? found.id : 0
}).filter((id: number) => id > 0)
} catch {
} finally {
modelsLoading.value = false
}
}
const transferOptions = computed(() => {
return allModels.value.map((m: any) => ({
value: m.id,
label: `${m.model_id}${m.model_name ? ` (${m.model_name})` : ''}`,
}))
})
async function handleSaveModels(done: () => void) {
if (!currentGroup.value) return
modelsLoading.value = true
try {
await request.put(`/admin/model-groups/${currentGroup.value.id}/models`, { model_ids: selectedModelIds.value })
Message.success('模型更新成功')
done()
fetchData()
} catch {
return false
} finally {
modelsLoading.value = false
}
}
onMounted(() => {
fetchData()
})
</script>
<template>
<div class="page-table">
<PageHeader title="模型分组" description="通过分组批量管理租户可用的模型,新增模型加入分组后自动对组内租户生效">
</PageHeader>
<ACard :bordered="false" class="mb-4">
<ARow :gutter="16" align="center">
<ACol :span="8">
<AInput v-model="filterSearch" placeholder="搜索分组名称或标识" allow-clear @clear="resetFilter" @press-enter="() => { pagination.current = 1; fetchData() }" />
</ACol>
<ACol :span="4">
<ASelect v-model="filterStatus" placeholder="全部状态" allow-clear @change="() => { pagination.current = 1; fetchData() }">
<AOption value="active">启用</AOption>
<AOption value="disabled">禁用</AOption>
</ASelect>
</ACol>
<ACol :span="4">
<AButton @click="() => { pagination.current = 1; fetchData() }">查询</AButton>
</ACol>
<ACol :span="8" style="text-align: right">
<AButton type="primary" @click="openCreate">创建分组</AButton>
</ACol>
</ARow>
</ACard>
<ACard :bordered="false">
<ATable
:columns="columns"
:data="data"
:loading="loading"
:bordered="false"
:stripe="true"
:pagination="false"
row-key="id"
:scroll="{ x: 1200 }"
/>
<div class="table-footer">
<APagination
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:show-page-size="pagination.showPageSize"
:page-size-options="pagination.pageSizeOptions"
show-total
@change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</ACard>
<!-- 创建/编辑分组弹窗 -->
<AModal
v-model:visible="showModal"
:title="modalTitle"
:on-before-ok="handleSubmit"
:ok-loading="formLoading"
>
<AForm :model="form" layout="vertical">
<AFormItem label="分组名称" required>
<AInput v-model="form.name" placeholder="如:全量模型、基础对话" />
</AFormItem>
<AFormItem v-if="!editingId" label="分组标识" required>
<AInput v-model="form.code" placeholder="如:full_access、basic_chat" />
</AFormItem>
<AFormItem label="描述">
<ATextarea v-model="form.description" placeholder="可选,描述分组用途" :auto-size="{ minRows: 2, maxRows: 4 }" />
</AFormItem>
</AForm>
</AModal>
<!-- 模型管理弹窗 -->
<AModal
v-model:visible="showModelsModal"
:title="`管理模型 - ${currentGroup?.name || ''}`"
:width="750"
:on-before-ok="handleSaveModels"
:ok-loading="modelsLoading"
>
<ASpin :loading="modelsLoading">
<ATransfer
v-model="selectedModelIds"
:data="transferOptions"
:title="['可选模型', '已选模型']"
searchable
/>
</ASpin>
</AModal>
</div>
</template>
<style scoped>
.page-table {
padding: 0 20px 20px;
}
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.mb-4 {
margin-bottom: 16px;
}
</style>
+117 -4
View File
@@ -286,11 +286,82 @@ async function removeModel(record: any) {
}
}
// === Tab 2b: Group Assignment ===
const groupsLoading = ref(false)
const tenantGroups = ref<any[]>([])
const allGroups = ref<any[]>([])
const showGroupModal = ref(false)
const groupAssignLoading = ref(false)
const selectedGroupIds = ref<number[]>([])
const groupColumns: TableColumnData[] = [
{ title: '分组名称', dataIndex: 'name', width: 160, ellipsis: true },
{ title: '标识', dataIndex: 'code', width: 140, ellipsis: true },
{ title: '状态', dataIndex: 'status', width: 80,
render({ record }) {
const color = record.status === 'active' ? 'green' : undefined
const label = record.status === 'active' ? '启用' : '禁用'
return h(Tag, { color, size: 'small' }, () => label)
},
},
{ title: '模型数', dataIndex: 'model_count', width: 80 },
]
async function fetchTenantGroups() {
groupsLoading.value = true
try {
const res: any = await request.get(`/admin/tenants/${tenantId}/groups`)
tenantGroups.value = res.data?.data?.list || res.data?.list || []
} catch {
} finally {
groupsLoading.value = false
}
}
async function fetchAllGroups() {
try {
const res: any = await request.get('/admin/model-groups/options')
allGroups.value = res.data?.data?.list || res.data?.list || []
} catch {
}
}
const groupTransferOptions = computed(() => {
return allGroups.value.map((g: any) => ({
value: g.id,
label: `${g.name}(${g.code})${g.model_count ? ` - ${g.model_count}个模型` : ''}`,
}))
})
function openGroupModal() {
selectedGroupIds.value = tenantGroups.value.map((g: any) => g.group_id)
showGroupModal.value = true
if (allGroups.value.length === 0) {
fetchAllGroups()
}
}
async function handleSaveGroups(done: () => void) {
groupAssignLoading.value = true
try {
await request.put(`/admin/tenants/${tenantId}/groups`, { group_ids: selectedGroupIds.value })
Message.success('分组更新成功')
done()
fetchTenantGroups()
fetchTenantModels()
} catch {
return false
} finally {
groupAssignLoading.value = false
}
}
// Load data when switching to models tab
function onTabChange(key: string) {
if (key === 'models') {
fetchTenantModels()
fetchAllModels()
fetchTenantGroups()
}
if (key === 'wallet') {
fetchWalletDetail()
@@ -510,12 +581,35 @@ onMounted(() => {
<!-- Tab 2: Model Assignment -->
<ATabPane key="models" title="模型分配">
<ACard :bordered="false">
<div class="flex items-center justify-between mb-4">
<span style="color: var(--ta-text-tertiary)">已分配 {{ modelsData.length }} 个模型</span>
<AButton type="primary" @click="openAssignModal">分配模型</AButton>
<ACard :bordered="false" class="mb-4" title="模型分组">
<template #extra>
<AButton type="primary" size="small" @click="openGroupModal">分配分组</AButton>
</template>
<div v-if="tenantGroups.length === 0" style="color: var(--ta-text-tertiary)">
暂未分配模型分组,租户可通过分组获取可用模型
</div>
<ATable
v-else
:columns="groupColumns"
:data="tenantGroups"
:loading="groupsLoading"
:bordered="false"
:stripe="true"
:pagination="false"
row-key="group_id"
size="small"
/>
</ACard>
<ACard :bordered="false" title="独立模型">
<template #extra>
<AButton type="primary" size="small" @click="openAssignModal">分配模型</AButton>
</template>
<div v-if="modelsData.length === 0" style="color: var(--ta-text-tertiary)">
暂无独立分配的模型
</div>
<ATable
v-else
:columns="modelColumns"
:data="modelsData"
:loading="modelsLoading"
@@ -584,6 +678,25 @@ onMounted(() => {
</template>
</ASpin>
<!-- Assign Groups Modal -->
<AModal
v-model:visible="showGroupModal"
title="分配模型分组"
:width="700"
:on-before-ok="handleSaveGroups"
:ok-loading="groupAssignLoading"
>
<div class="mb-3 text-sm" style="color: var(--ta-text-tertiary)">
选择分组后,分组内的所有模型自动对该租户可用
</div>
<ATransfer
v-model="selectedGroupIds"
:data="groupTransferOptions"
:title="['可选分组', '已选分组']"
searchable
/>
</AModal>
<!-- Assign Models Modal -->
<AModal
v-model:visible="showAssignModal"