feat(group):新增模型分组功能
This commit is contained in:
1 parent
b9d20d6b31
commit
09f1b30db4
37 files changed
+1938
-24
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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:"导出模型列表"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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:返回所有活跃模型(公开端点场景)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 // 更新时间
|
||||
}
|
||||
@@ -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 // 更新时间
|
||||
}
|
||||
@@ -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 // 更新时间
|
||||
}
|
||||
@@ -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:"更新时间"` // 更新时间
|
||||
}
|
||||
@@ -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:"更新时间"` // 更新时间
|
||||
}
|
||||
@@ -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 获取单个通知模板
|
||||
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user