From 09f1b30db40b414df122e21858eae2884a7beb12 Mon Sep 17 00:00:00 2001 From: qian Date: Wed, 27 May 2026 20:52:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(group):=E6=96=B0=E5=A2=9E=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=88=86=E7=BB=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin/admin.go | 10 + api/admin/v1/model.go | 20 + api/admin/v1/model_group.go | 154 ++++++++ internal/consts/consts.go | 10 + .../admin/admin_v1_group_models_list.go | 12 + .../admin/admin_v1_group_models_set.go | 12 + .../admin/admin_v1_model_group_create.go | 12 + .../admin/admin_v1_model_group_delete.go | 12 + .../admin/admin_v1_model_group_list.go | 12 + .../admin/admin_v1_model_group_options.go | 12 + .../admin/admin_v1_model_group_update.go | 12 + .../admin/admin_v1_model_options.go | 12 + .../admin/admin_v1_tenant_groups_list.go | 12 + .../admin/admin_v1_tenant_groups_set.go | 12 + internal/dao/internal/mdl_group_models.go | 87 +++++ internal/dao/internal/mdl_model_groups.go | 91 +++++ internal/dao/internal/mdl_tenant_groups.go | 87 +++++ internal/dao/mdl_group_models.go | 22 ++ internal/dao/mdl_model_groups.go | 22 ++ internal/dao/mdl_tenant_groups.go | 22 ++ internal/logic/admin/model.go | 53 ++- internal/logic/admin/model_group.go | 356 ++++++++++++++++++ internal/logic/common/cache.go | 3 + internal/logic/relay/provider.go | 166 +++++++- internal/logic/tenant/model.go | 73 ++++ internal/model/do/mdl_group_models.go | 20 + internal/model/do/mdl_model_groups.go | 22 ++ internal/model/do/mdl_tenant_groups.go | 20 + internal/model/entity/mdl_group_models.go | 18 + internal/model/entity/mdl_model_groups.go | 20 + internal/model/entity/mdl_tenant_groups.go | 18 + internal/service/admin.go | 22 +- migrations/000006_model_groups.sql | 66 ++++ web/admin/src/router/admin.ts | 6 + web/admin/src/views/AdminLayout.vue | 1 + web/admin/src/views/ai/ModelGroupsPage.vue | 332 ++++++++++++++++ .../src/views/tenant/TenantDetailPage.vue | 121 +++++- 37 files changed, 1938 insertions(+), 24 deletions(-) create mode 100644 api/admin/v1/model_group.go create mode 100644 internal/controller/admin/admin_v1_group_models_list.go create mode 100644 internal/controller/admin/admin_v1_group_models_set.go create mode 100644 internal/controller/admin/admin_v1_model_group_create.go create mode 100644 internal/controller/admin/admin_v1_model_group_delete.go create mode 100644 internal/controller/admin/admin_v1_model_group_list.go create mode 100644 internal/controller/admin/admin_v1_model_group_options.go create mode 100644 internal/controller/admin/admin_v1_model_group_update.go create mode 100644 internal/controller/admin/admin_v1_model_options.go create mode 100644 internal/controller/admin/admin_v1_tenant_groups_list.go create mode 100644 internal/controller/admin/admin_v1_tenant_groups_set.go create mode 100644 internal/dao/internal/mdl_group_models.go create mode 100644 internal/dao/internal/mdl_model_groups.go create mode 100644 internal/dao/internal/mdl_tenant_groups.go create mode 100644 internal/dao/mdl_group_models.go create mode 100644 internal/dao/mdl_model_groups.go create mode 100644 internal/dao/mdl_tenant_groups.go create mode 100644 internal/logic/admin/model_group.go create mode 100644 internal/model/do/mdl_group_models.go create mode 100644 internal/model/do/mdl_model_groups.go create mode 100644 internal/model/do/mdl_tenant_groups.go create mode 100644 internal/model/entity/mdl_group_models.go create mode 100644 internal/model/entity/mdl_model_groups.go create mode 100644 internal/model/entity/mdl_tenant_groups.go create mode 100644 migrations/000006_model_groups.sql create mode 100644 web/admin/src/views/ai/ModelGroupsPage.vue diff --git a/api/admin/admin.go b/api/admin/admin.go index a981171..a8e560c 100644 --- a/api/admin/admin.go +++ b/api/admin/admin.go @@ -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) diff --git a/api/admin/v1/model.go b/api/admin/v1/model.go index 1cf2cb3..2c391c7 100644 --- a/api/admin/v1/model.go +++ b/api/admin/v1/model.go @@ -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:"导出模型列表"` diff --git a/api/admin/v1/model_group.go b/api/admin/v1/model_group.go new file mode 100644 index 0000000..bb5f9ae --- /dev/null +++ b/api/admin/v1/model_group.go @@ -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"` +} diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 3738e27..112bd3c 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -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 diff --git a/internal/controller/admin/admin_v1_group_models_list.go b/internal/controller/admin/admin_v1_group_models_list.go new file mode 100644 index 0000000..2b0a39e --- /dev/null +++ b/internal/controller/admin/admin_v1_group_models_list.go @@ -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) +} diff --git a/internal/controller/admin/admin_v1_group_models_set.go b/internal/controller/admin/admin_v1_group_models_set.go new file mode 100644 index 0000000..3561acb --- /dev/null +++ b/internal/controller/admin/admin_v1_group_models_set.go @@ -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) +} diff --git a/internal/controller/admin/admin_v1_model_group_create.go b/internal/controller/admin/admin_v1_model_group_create.go new file mode 100644 index 0000000..86d71d8 --- /dev/null +++ b/internal/controller/admin/admin_v1_model_group_create.go @@ -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) +} diff --git a/internal/controller/admin/admin_v1_model_group_delete.go b/internal/controller/admin/admin_v1_model_group_delete.go new file mode 100644 index 0000000..ca99acd --- /dev/null +++ b/internal/controller/admin/admin_v1_model_group_delete.go @@ -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) +} diff --git a/internal/controller/admin/admin_v1_model_group_list.go b/internal/controller/admin/admin_v1_model_group_list.go new file mode 100644 index 0000000..b95bac2 --- /dev/null +++ b/internal/controller/admin/admin_v1_model_group_list.go @@ -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) +} diff --git a/internal/controller/admin/admin_v1_model_group_options.go b/internal/controller/admin/admin_v1_model_group_options.go new file mode 100644 index 0000000..d5b6de2 --- /dev/null +++ b/internal/controller/admin/admin_v1_model_group_options.go @@ -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) +} diff --git a/internal/controller/admin/admin_v1_model_group_update.go b/internal/controller/admin/admin_v1_model_group_update.go new file mode 100644 index 0000000..467fd26 --- /dev/null +++ b/internal/controller/admin/admin_v1_model_group_update.go @@ -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) +} diff --git a/internal/controller/admin/admin_v1_model_options.go b/internal/controller/admin/admin_v1_model_options.go new file mode 100644 index 0000000..a8e246b --- /dev/null +++ b/internal/controller/admin/admin_v1_model_options.go @@ -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) +} diff --git a/internal/controller/admin/admin_v1_tenant_groups_list.go b/internal/controller/admin/admin_v1_tenant_groups_list.go new file mode 100644 index 0000000..fbf86df --- /dev/null +++ b/internal/controller/admin/admin_v1_tenant_groups_list.go @@ -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) +} diff --git a/internal/controller/admin/admin_v1_tenant_groups_set.go b/internal/controller/admin/admin_v1_tenant_groups_set.go new file mode 100644 index 0000000..9686f91 --- /dev/null +++ b/internal/controller/admin/admin_v1_tenant_groups_set.go @@ -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) +} diff --git a/internal/dao/internal/mdl_group_models.go b/internal/dao/internal/mdl_group_models.go new file mode 100644 index 0000000..81b2d2a --- /dev/null +++ b/internal/dao/internal/mdl_group_models.go @@ -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) +} diff --git a/internal/dao/internal/mdl_model_groups.go b/internal/dao/internal/mdl_model_groups.go new file mode 100644 index 0000000..e12ac3a --- /dev/null +++ b/internal/dao/internal/mdl_model_groups.go @@ -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) +} diff --git a/internal/dao/internal/mdl_tenant_groups.go b/internal/dao/internal/mdl_tenant_groups.go new file mode 100644 index 0000000..3859266 --- /dev/null +++ b/internal/dao/internal/mdl_tenant_groups.go @@ -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) +} diff --git a/internal/dao/mdl_group_models.go b/internal/dao/mdl_group_models.go new file mode 100644 index 0000000..c19b792 --- /dev/null +++ b/internal/dao/mdl_group_models.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" +) + +// 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. diff --git a/internal/dao/mdl_model_groups.go b/internal/dao/mdl_model_groups.go new file mode 100644 index 0000000..78b7ea4 --- /dev/null +++ b/internal/dao/mdl_model_groups.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" +) + +// 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. diff --git a/internal/dao/mdl_tenant_groups.go b/internal/dao/mdl_tenant_groups.go new file mode 100644 index 0000000..0ddc04a --- /dev/null +++ b/internal/dao/mdl_tenant_groups.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" +) + +// 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. diff --git a/internal/logic/admin/model.go b/internal/logic/admin/model.go index ac75130..c6da3f6 100644 --- a/internal/logic/admin/model.go +++ b/internal/logic/admin/model.go @@ -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 diff --git a/internal/logic/admin/model_group.go b/internal/logic/admin/model_group.go new file mode 100644 index 0000000..775d906 --- /dev/null +++ b/internal/logic/admin/model_group.go @@ -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) + } +} diff --git a/internal/logic/common/cache.go b/internal/logic/common/cache.go index 337d9e0..53b3527 100644 --- a/internal/logic/common/cache.go +++ b/internal/logic/common/cache.go @@ -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) diff --git a/internal/logic/relay/provider.go b/internal/logic/relay/provider.go index a5ebb55..4a69af1 100644 --- a/internal/logic/relay/provider.go +++ b/internal/logic/relay/provider.go @@ -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:返回所有活跃模型(公开端点场景) diff --git a/internal/logic/tenant/model.go b/internal/logic/tenant/model.go index 0429bfd..57d8439 100644 --- a/internal/logic/tenant/model.go +++ b/internal/logic/tenant/model.go @@ -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 } diff --git a/internal/model/do/mdl_group_models.go b/internal/model/do/mdl_group_models.go new file mode 100644 index 0000000..8d79b93 --- /dev/null +++ b/internal/model/do/mdl_group_models.go @@ -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 // 更新时间 +} diff --git a/internal/model/do/mdl_model_groups.go b/internal/model/do/mdl_model_groups.go new file mode 100644 index 0000000..2a2b3e0 --- /dev/null +++ b/internal/model/do/mdl_model_groups.go @@ -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 // 更新时间 +} diff --git a/internal/model/do/mdl_tenant_groups.go b/internal/model/do/mdl_tenant_groups.go new file mode 100644 index 0000000..79c4782 --- /dev/null +++ b/internal/model/do/mdl_tenant_groups.go @@ -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 // 更新时间 +} diff --git a/internal/model/entity/mdl_group_models.go b/internal/model/entity/mdl_group_models.go new file mode 100644 index 0000000..06c09f9 --- /dev/null +++ b/internal/model/entity/mdl_group_models.go @@ -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:"更新时间"` // 更新时间 +} diff --git a/internal/model/entity/mdl_model_groups.go b/internal/model/entity/mdl_model_groups.go new file mode 100644 index 0000000..9b549f2 --- /dev/null +++ b/internal/model/entity/mdl_model_groups.go @@ -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:"更新时间"` // 更新时间 +} diff --git a/internal/model/entity/mdl_tenant_groups.go b/internal/model/entity/mdl_tenant_groups.go new file mode 100644 index 0000000..6e21fdf --- /dev/null +++ b/internal/model/entity/mdl_tenant_groups.go @@ -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:"更新时间"` // 更新时间 +} diff --git a/internal/service/admin.go b/internal/service/admin.go index 250b2cb..1adc420 100644 --- a/internal/service/admin.go +++ b/internal/service/admin.go @@ -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 获取单个通知模板 diff --git a/migrations/000006_model_groups.sql b/migrations/000006_model_groups.sql new file mode 100644 index 0000000..50f39c0 --- /dev/null +++ b/migrations/000006_model_groups.sql @@ -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; diff --git a/web/admin/src/router/admin.ts b/web/admin/src/router/admin.ts index 0fc0bc9..8f6af9b 100644 --- a/web/admin/src/router/admin.ts +++ b/web/admin/src/router/admin.ts @@ -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', diff --git a/web/admin/src/views/AdminLayout.vue b/web/admin/src/views/AdminLayout.vue index 5441342..407ae99 100644 --- a/web/admin/src/views/AdminLayout.vue +++ b/web/admin/src/views/AdminLayout.vue @@ -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 }, diff --git a/web/admin/src/views/ai/ModelGroupsPage.vue b/web/admin/src/views/ai/ModelGroupsPage.vue new file mode 100644 index 0000000..0e956fe --- /dev/null +++ b/web/admin/src/views/ai/ModelGroupsPage.vue @@ -0,0 +1,332 @@ + + + + + diff --git a/web/admin/src/views/tenant/TenantDetailPage.vue b/web/admin/src/views/tenant/TenantDetailPage.vue index f0a5359..e20e1b6 100644 --- a/web/admin/src/views/tenant/TenantDetailPage.vue +++ b/web/admin/src/views/tenant/TenantDetailPage.vue @@ -286,11 +286,82 @@ async function removeModel(record: any) { } } +// === Tab 2b: Group Assignment === +const groupsLoading = ref(false) +const tenantGroups = ref([]) +const allGroups = ref([]) +const showGroupModal = ref(false) +const groupAssignLoading = ref(false) +const selectedGroupIds = ref([]) + +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(() => { - -
- 已分配 {{ modelsData.length }} 个模型 - 分配模型 + + +
+ 暂未分配模型分组,租户可通过分组获取可用模型
+
+ + + +
+ 暂无独立分配的模型 +
+ { + + +
+ 选择分组后,分组内的所有模型自动对该租户可用 +
+ +
+