feat(tenant):新增用户等级功能

This commit is contained in:
qian committed 2026-05-26 08:01:41 +08:00
1 parent ded149ed2f
commit f3e87866c4
49 files changed
+963 -156

No files matched your search

+4
View File
@@ -208,6 +208,10 @@ type IAdminV1 interface {
TenantExport(ctx context.Context, req *v1.TenantExportReq) (res *v1.TenantExportRes, err error)
AdminMemberExport(ctx context.Context, req *v1.AdminMemberExportReq) (res *v1.AdminMemberExportRes, err error)
TenantSelect(ctx context.Context, req *v1.TenantSelectReq) (res *v1.TenantSelectRes, err error)
TenantLevelConfigList(ctx context.Context, req *v1.TenantLevelConfigListReq) (res *v1.TenantLevelConfigListRes, err error)
TenantLevelConfigCreate(ctx context.Context, req *v1.TenantLevelConfigCreateReq) (res *v1.TenantLevelConfigCreateRes, err error)
TenantLevelConfigUpdate(ctx context.Context, req *v1.TenantLevelConfigUpdateReq) (res *v1.TenantLevelConfigUpdateRes, err error)
TenantLevelConfigDelete(ctx context.Context, req *v1.TenantLevelConfigDeleteReq) (res *v1.TenantLevelConfigDeleteRes, err error)
TenantModelList(ctx context.Context, req *v1.TenantModelListReq) (res *v1.TenantModelListRes, err error)
TenantModelBatchAssign(ctx context.Context, req *v1.TenantModelBatchAssignReq) (res *v1.TenantModelBatchAssignRes, err error)
TenantModelUpdate(ctx context.Context, req *v1.TenantModelUpdateReq) (res *v1.TenantModelUpdateRes, err error)
+3
View File
@@ -47,6 +47,8 @@ type TenantItem struct {
DefaultChannelScope string `json:"default_channel_scope"`
MemberCount int `json:"member_count"`
WalletBalance string `json:"wallet_balance"`
Level int `json:"level"`
LevelName string `json:"level_name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
@@ -85,6 +87,7 @@ type TenantUpdateReq struct {
Name string `json:"name" dc:"租户名称"`
MaxMembers *int `json:"max_members" dc:"最大成员数"`
MaxConcurrency *int `json:"max_concurrency" dc:"租户总并发上限(0表示不限制)"`
Level *int `json:"level" dc:"租户等级(调整等级会同步更新成员数和并发数为该等级的配置值)"`
}
type TenantUpdateRes struct{}
+59
View File
@@ -0,0 +1,59 @@
package v1
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// === 租户等级配置管理 ===
type TenantLevelConfigItem struct {
Id int64 `json:"id"`
Level int `json:"level"`
Name string `json:"name"`
CumulativeRechargeThreshold float64 `json:"cumulative_recharge_threshold"`
MaxMembers int `json:"max_members"`
MaxConcurrency int `json:"max_concurrency"`
PriceMultiplier float64 `json:"price_multiplier"`
SortOrder int `json:"sort_order"`
CreatedAt *gtime.Time `json:"created_at"`
UpdatedAt *gtime.Time `json:"updated_at"`
}
type TenantLevelConfigListReq struct {
g.Meta `path:"/tenant-level-configs" method:"get" mime:"json" tags:"管理后台-租户等级" summary:"等级配置列表"`
}
type TenantLevelConfigListRes struct {
List []*TenantLevelConfigItem `json:"list"`
}
type TenantLevelConfigCreateReq struct {
g.Meta `path:"/tenant-level-configs" method:"post" mime:"json" tags:"管理后台-租户等级" summary:"创建等级配置"`
Level int `json:"level" v:"required|min:1#请输入等级号|等级号最小为1"`
Name string `json:"name" v:"required#请输入等级名称"`
CumulativeRechargeThreshold float64 `json:"cumulative_recharge_threshold" v:"required|min:0#请输入累计充值阈值|阈值不能为负数"`
MaxMembers int `json:"max_members" v:"min:0#最大成员数不能为负数"`
MaxConcurrency int `json:"max_concurrency" v:"min:0#最大并发数不能为负数"`
PriceMultiplier float64 `json:"price_multiplier" d:"1.0000"`
SortOrder int `json:"sort_order" d:"0"`
}
type TenantLevelConfigCreateRes struct {
ID int64 `json:"id"`
}
type TenantLevelConfigUpdateReq struct {
g.Meta `path:"/tenant-level-configs/{id}" method:"put" mime:"json" tags:"管理后台-租户等级" summary:"更新等级配置"`
Id int64 `json:"id" in:"path" v:"required|min:1"`
Update map[string]interface{} `json:"update" v:"required"`
}
type TenantLevelConfigUpdateRes struct{}
type TenantLevelConfigDeleteReq struct {
g.Meta `path:"/tenant-level-configs/{id}" method:"delete" mime:"json" tags:"管理后台-租户等级" summary:"删除等级配置"`
Id int64 `json:"id" in:"path" v:"required|min:1"`
}
type TenantLevelConfigDeleteRes struct{}
+2
View File
@@ -13,6 +13,8 @@ type TenantOrgInfoRes struct {
Code string `json:"code"`
LogoURL string `json:"logo_url"`
Status string `json:"status"`
Level int `json:"level"`
LevelName string `json:"level_name"`
MaxMembers int `json:"max_members"`
MemberCount int `json:"member_count"`
CreatedAt string `json:"created_at"`
+10 -5
View File
@@ -9,11 +9,16 @@ type TenantWalletReq struct {
}
type TenantWalletRes struct {
Balance float64 `json:"balance"`
FrozenBalance float64 `json:"frozen_balance"`
AvailableBalance float64 `json:"available_balance"`
WarningThreshold float64 `json:"warning_threshold"`
Currency string `json:"currency"`
Balance float64 `json:"balance"`
FrozenBalance float64 `json:"frozen_balance"`
AvailableBalance float64 `json:"available_balance"`
WarningThreshold float64 `json:"warning_threshold"`
Currency string `json:"currency"`
Level int `json:"level"`
LevelName string `json:"level_name"`
CumulativeRecharge float64 `json:"cumulative_recharge"`
NextLevelThreshold float64 `json:"next_level_threshold"`
NextLevelName string `json:"next_level_name"`
}
type TenantWalletTransactionsReq struct {
@@ -0,0 +1,12 @@
package admin
import (
"context"
v1 "github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) TenantLevelConfigCreate(ctx context.Context, req *v1.TenantLevelConfigCreateReq) (res *v1.TenantLevelConfigCreateRes, err error) {
return service.Admin().CreateTenantLevelConfig(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
v1 "github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) TenantLevelConfigDelete(ctx context.Context, req *v1.TenantLevelConfigDeleteReq) (res *v1.TenantLevelConfigDeleteRes, err error) {
return service.Admin().DeleteTenantLevelConfig(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
v1 "github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) TenantLevelConfigList(ctx context.Context, req *v1.TenantLevelConfigListReq) (res *v1.TenantLevelConfigListRes, err error) {
return service.Admin().ListTenantLevelConfigs(ctx, req)
}
@@ -0,0 +1,12 @@
package admin
import (
"context"
v1 "github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) TenantLevelConfigUpdate(ctx context.Context, req *v1.TenantLevelConfigUpdateReq) (res *v1.TenantLevelConfigUpdateRes, err error) {
return service.Admin().UpdateTenantLevelConfig(ctx, req)
}
@@ -25,6 +25,7 @@ type AudContentFilterLogsColumns struct {
TenantId string // 租户ID
UserId string // 用户ID
ApiKeyId string // API Key ID
ProjectId string // 项目ID
RequestId string // 请求唯一ID
Method string // HTTP 方法
Path string // 请求路径
@@ -34,7 +35,6 @@ type AudContentFilterLogsColumns struct {
OriginalSnippet string // 原始请求体片段(截断存储,仅 replace 模式)
Blocked string // 是否被拦截(mode=block 时为 true)
CreatedAt string // 创建时间
ProjectId string // 项目ID
}
// audContentFilterLogsColumns holds the columns for the table aud_content_filter_logs.
@@ -43,6 +43,7 @@ var audContentFilterLogsColumns = AudContentFilterLogsColumns{
TenantId: "tenant_id",
UserId: "user_id",
ApiKeyId: "api_key_id",
ProjectId: "project_id",
RequestId: "request_id",
Method: "method",
Path: "path",
@@ -52,7 +53,6 @@ var audContentFilterLogsColumns = AudContentFilterLogsColumns{
OriginalSnippet: "original_snippet",
Blocked: "blocked",
CreatedAt: "created_at",
ProjectId: "project_id",
}
// NewAudContentFilterLogsDao creates and returns a new DAO object for table data access.
+18 -16
View File
@@ -21,26 +21,28 @@ type BilWalletsDao struct {
// BilWalletsColumns defines and stores column names for the table bil_wallets.
type BilWalletsColumns struct {
Id string // 主键ID
TenantId string // 租户ID(每个租户一个钱包)
Balance string // 总余额
FrozenBalance string // 冻结余额(支付中/退款中,可用余额 = balance - frozen_balance)
WarningThreshold string // 余额预警线(低于此值触发通知)
Currency string // 货币(USD)
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
Id string // 主键ID
TenantId string // 租户ID(每个租户一个钱包)
Balance string // 总余额
FrozenBalance string // 冻结余额(支付中/退款中,可用余额 = balance - frozen_balance)
WarningThreshold string // 余额预警线(低于此值触发通知)
Currency string // 货币(USD)
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
CumulativeRecharge string // 累计充值总额(USD)
}
// bilWalletsColumns holds the columns for the table bil_wallets.
var bilWalletsColumns = BilWalletsColumns{
Id: "id",
TenantId: "tenant_id",
Balance: "balance",
FrozenBalance: "frozen_balance",
WarningThreshold: "warning_threshold",
Currency: "currency",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
Id: "id",
TenantId: "tenant_id",
Balance: "balance",
FrozenBalance: "frozen_balance",
WarningThreshold: "warning_threshold",
Currency: "currency",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
CumulativeRecharge: "cumulative_recharge",
}
// NewBilWalletsDao creates and returns a new DAO object for table data access.
+2 -2
View File
@@ -29,9 +29,9 @@ type ChnChannelKeysColumns struct {
LastUsedAt string // 最后使用时间
LastError string // 最后一次错误信息
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
KeyType string // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)
TokenExpiresAt string // OAuth access_token 过期时间(仅 key_type=oauth 时有值)
UpdatedAt string // 更新时间
}
// chnChannelKeysColumns holds the columns for the table chn_channel_keys.
@@ -44,9 +44,9 @@ var chnChannelKeysColumns = ChnChannelKeysColumns{
LastUsedAt: "last_used_at",
LastError: "last_error",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
KeyType: "key_type",
TokenExpiresAt: "token_expires_at",
UpdatedAt: "updated_at",
}
// NewChnChannelKeysDao creates and returns a new DAO object for table data access.
+2 -2
View File
@@ -30,12 +30,12 @@ type MdlModelsColumns struct {
MaxOutputTokens string // 最大输出 token 数
Description string // 模型描述
Tags string // 标签(如 reasoning、vision、function_calling)
Capabilities string // 模型能力特性(如 vision、function_calling、reasoning 等)
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeprecatedAt string // 标记弃用的时间(NULL表示未弃用)
SunsetDate string // 计划下线日期(到达后返回410 Gone,NULL表示未设置)
ReplacementModel string // 推荐替代模型名(NULL表示无替代)
Capabilities string // 模型能力特性(如 vision、function_calling、reasoning 等)
}
// mdlModelsColumns holds the columns for the table mdl_models.
@@ -49,12 +49,12 @@ var mdlModelsColumns = MdlModelsColumns{
MaxOutputTokens: "max_output_tokens",
Description: "description",
Tags: "tags",
Capabilities: "capabilities",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeprecatedAt: "deprecated_at",
SunsetDate: "sunset_date",
ReplacementModel: "replacement_model",
Capabilities: "capabilities",
}
// NewMdlModelsDao creates and returns a new DAO object for table data access.
+2 -2
View File
@@ -31,8 +31,8 @@ type NtfMessagesColumns struct {
IsRead string // 是否已读:0=未读, 1=已读
IsBroadcast string // 是否广播消息:0=个人消息, 1=广播消息
Metadata string // 附加元数据(JSONB,如关联资源ID、跳转链接等)
CreatedAt string // 创建时间
TargetRoles string // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
CreatedAt string // 创建时间
}
// ntfMessagesColumns holds the columns for the table ntf_messages.
@@ -47,8 +47,8 @@ var ntfMessagesColumns = NtfMessagesColumns{
IsRead: "is_read",
IsBroadcast: "is_broadcast",
Metadata: "metadata",
CreatedAt: "created_at",
TargetRoles: "target_roles",
CreatedAt: "created_at",
}
// NewNtfMessagesDao creates and returns a new DAO object for table data access.
@@ -0,0 +1,97 @@
// ==========================================================================
// 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"
)
// TntTenantLevelConfigsDao is the data access object for the table tnt_tenant_level_configs.
type TntTenantLevelConfigsDao 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 TntTenantLevelConfigsColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// TntTenantLevelConfigsColumns defines and stores column names for the table tnt_tenant_level_configs.
type TntTenantLevelConfigsColumns struct {
Id string //
Level string // 等级号(1, 2, 3...)
Name string // 等级名称
CumulativeRechargeThreshold string // 累计充值阈值(USD),达到此值自动升级
MaxMembers string // 该等级最大成员数
MaxConcurrency string // 该等级最大并发数,0=无限
PriceMultiplier string // 价格乘数(折扣,如 0.9=九折)
SortOrder string // 排序权重
CreatedAt string //
UpdatedAt string //
}
// tntTenantLevelConfigsColumns holds the columns for the table tnt_tenant_level_configs.
var tntTenantLevelConfigsColumns = TntTenantLevelConfigsColumns{
Id: "id",
Level: "level",
Name: "name",
CumulativeRechargeThreshold: "cumulative_recharge_threshold",
MaxMembers: "max_members",
MaxConcurrency: "max_concurrency",
PriceMultiplier: "price_multiplier",
SortOrder: "sort_order",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
}
// NewTntTenantLevelConfigsDao creates and returns a new DAO object for table data access.
func NewTntTenantLevelConfigsDao(handlers ...gdb.ModelHandler) *TntTenantLevelConfigsDao {
return &TntTenantLevelConfigsDao{
group: "default",
table: "tnt_tenant_level_configs",
columns: tntTenantLevelConfigsColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *TntTenantLevelConfigsDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *TntTenantLevelConfigsDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *TntTenantLevelConfigsDao) Columns() TntTenantLevelConfigsColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *TntTenantLevelConfigsDao) 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 *TntTenantLevelConfigsDao) 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 *TntTenantLevelConfigsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
+2
View File
@@ -38,6 +38,7 @@ type TntTenantsColumns struct {
DataRemovalAt string // 数据清除时间(冻结 30 天后)
MaxConcurrency string // 租户总并发上限(0表示不限制)
DefaultChannelScope string // 默认渠道范围(NULL或[]表示全部可用,否则为channel_id数组)
Level string // 当前等级(对应 tnt_tenant_level_configs.level)
}
// tntTenantsColumns holds the columns for the table tnt_tenants.
@@ -59,6 +60,7 @@ var tntTenantsColumns = TntTenantsColumns{
DataRemovalAt: "data_removal_at",
MaxConcurrency: "max_concurrency",
DefaultChannelScope: "default_channel_scope",
Level: "level",
}
// NewTntTenantsDao creates and returns a new DAO object for table data access.
+24 -24
View File
@@ -21,30 +21,30 @@ type TskModelTasksDao struct {
// TskModelTasksColumns defines and stores column names for the table tsk_model_tasks.
type TskModelTasksColumns struct {
Id string //
PublicTaskId string //
Platform string //
Action string //
Status string //
Progress string //
FailReason string //
TenantId string //
UserId string //
ApiKeyId string //
ChannelId string //
ModelName string //
UpstreamModel string //
PreDeductAmount string //
ActualCost string //
BillingSettled string //
ResultUrl string //
Data string //
PrivateData string //
SubmitTime string //
StartTime string //
FinishTime string //
CreatedAt string //
UpdatedAt string //
Id string // 主键ID
PublicTaskId string // 对外公开的任务ID(task_xxxxx 格式),用于 API 响应和客户端查询
Platform string // 任务所属平台:sora、kling、suno、midjourney、volcengine
Action string // 任务动作类型。通用:generate;Suno:music、lyrics;
Status string // 任务状态:NOT_START-未开始、SUBMITTED-已提交、IN_PROGRESS-进行中、SUCCESS-成功、FAILURE-失败
Progress string // 任务进度,字符串格式百分比(如 0%、50%、100%)
FailReason string // 任务失败原因文本
TenantId string // 发起任务的租户ID
UserId string // 发起任务的租户用户ID
ApiKeyId string // 调用方使用的 API Key ID
ChannelId string // 转发请求的渠道ID
ModelName string // 用户请求的模型名称(如 sora-1.0-turbo、midjourney 等)
UpstreamModel string // 上游供应商实际使用的模型名称,可能与请求模型不同
PreDeductAmount string // 提交任务时的预扣金额(USD),任务完成后根据实际用量结算差额
ActualCost string // 任务实际消费金额(USD),成功时按此结算,失败时退还预扣金额
BillingSettled string // 计费是否已结算(true 表示已完成预扣与实际金额的多退少补)
ResultUrl string // 任务完成后的结果资源 URL(如生成的视频/图片/音频的下载地址)
Data string // 上游供应商返回的完整响应数据(JSONB),可返回给用户查看
PrivateData string // 内部私有数据(JSONB),含 upstream_task_id 等敏感字段,用于轮询上游状态,不返回给用户
SubmitTime string // 任务提交到上游供应商的时间
StartTime string // 上游供应商开始执行任务的时间
FinishTime string // 任务完成(成功或失败)的时间
CreatedAt string // 记录创建时间
UpdatedAt string // 记录更新时间
RequestId string // 任务提交时的原始请求 ID(req_xxxxx),关联 aud_request_logs.request_id
}
+22
View File
@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"github.com/qianfree/team-api/internal/dao/internal"
)
// tntTenantLevelConfigsDao is the data access object for the table tnt_tenant_level_configs.
// You can define custom methods on it to extend its functionality as needed.
type tntTenantLevelConfigsDao struct {
*internal.TntTenantLevelConfigsDao
}
var (
// TntTenantLevelConfigs is a globally accessible object for table tnt_tenant_level_configs operations.
TntTenantLevelConfigs = tntTenantLevelConfigsDao{internal.NewTntTenantLevelConfigsDao()}
)
// Add your custom methods and functionality below.
+28
View File
@@ -14,6 +14,7 @@ import (
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
do "github.com/qianfree/team-api/internal/model/do"
"github.com/qianfree/team-api/internal/model/entity"
"github.com/qianfree/team-api/internal/utility/crypto"
"github.com/qianfree/team-api/internal/utility/export"
)
@@ -118,6 +119,7 @@ func (s *sAdmin) CreateTenant(ctx context.Context, req *v1.TenantCreateReq) (*v1
Code: tenantCode,
MaxMembers: maxMembers,
MaxConcurrency: maxConcurrency,
Level: 1,
Settings: "{}",
}).Insert()
if err != nil {
@@ -289,6 +291,7 @@ func (s *sAdmin) GetTenant(ctx context.Context, req *v1.TenantGetReq) (*v1.Tenan
MaxConcurrency int `json:"max_concurrency"`
DefaultChannelScope string `json:"default_channel_scope"`
Settings string `json:"settings"`
Level int `json:"level"`
CreatedAt *gtime.Time `json:"created_at"`
UpdatedAt *gtime.Time `json:"updated_at"`
}
@@ -327,6 +330,15 @@ func (s *sAdmin) GetTenant(ctx context.Context, req *v1.TenantGetReq) (*v1.Tenan
if owner != nil {
ownerName = owner.DisplayName
}
// Get level name
levelName := ""
var levelConfig entity.TntTenantLevelConfigs
_ = dao.TntTenantLevelConfigs.Ctx(ctx).Where("level", tenant.Level).Scan(&levelConfig)
if levelConfig.Id > 0 {
levelName = levelConfig.Name
}
return &v1.TenantGetRes{
TenantItem: v1.TenantItem{
ID: tenant.Id,
@@ -341,6 +353,8 @@ func (s *sAdmin) GetTenant(ctx context.Context, req *v1.TenantGetReq) (*v1.Tenan
DefaultChannelScope: tenant.DefaultChannelScope,
MemberCount: memberCount,
WalletBalance: walletBalance,
Level: tenant.Level,
LevelName: levelName,
CreatedAt: tenant.CreatedAt.String(),
UpdatedAt: tenant.UpdatedAt.String(),
},
@@ -394,6 +408,20 @@ func (s *sAdmin) UpdateTenant(ctx context.Context, req *v1.TenantUpdateReq) (*v1
data.MaxConcurrency = *req.MaxConcurrency
}
// 管理员手动调整等级:同步更新 max_members 和 max_concurrency
if req.Level != nil {
data.Level = *req.Level
var config entity.TntTenantLevelConfigs
err := dao.TntTenantLevelConfigs.Ctx(ctx).Where("level", *req.Level).Scan(&config)
if err != nil {
return nil, common.NewBadRequestError("等级配置不存在")
}
if config.Id > 0 {
data.MaxMembers = config.MaxMembers
data.MaxConcurrency = config.MaxConcurrency
}
}
_, err := dao.TntTenants.Ctx(ctx).Where("id", req.Id).Update(data)
if err != nil {
return nil, err
+96
View File
@@ -0,0 +1,96 @@
package admin
import (
"context"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/model/do"
"github.com/qianfree/team-api/internal/model/entity"
)
func (s *sAdmin) ListTenantLevelConfigs(ctx context.Context, _ *v1.TenantLevelConfigListReq) (*v1.TenantLevelConfigListRes, error) {
var configs []*entity.TntTenantLevelConfigs
err := dao.TntTenantLevelConfigs.Ctx(ctx).
OrderAsc("level").
Scan(&configs)
if err != nil {
return nil, err
}
list := make([]*v1.TenantLevelConfigItem, 0, len(configs))
for _, c := range configs {
list = append(list, &v1.TenantLevelConfigItem{
Id: c.Id,
Level: c.Level,
Name: c.Name,
CumulativeRechargeThreshold: c.CumulativeRechargeThreshold,
MaxMembers: c.MaxMembers,
MaxConcurrency: c.MaxConcurrency,
PriceMultiplier: c.PriceMultiplier,
SortOrder: c.SortOrder,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
})
}
return &v1.TenantLevelConfigListRes{List: list}, nil
}
func (s *sAdmin) CreateTenantLevelConfig(ctx context.Context, req *v1.TenantLevelConfigCreateReq) (*v1.TenantLevelConfigCreateRes, error) {
// 检查 level 是否已存在
count, _ := dao.TntTenantLevelConfigs.Ctx(ctx).Where("level", req.Level).Count()
if count > 0 {
return nil, gerror.New("等级号已存在")
}
result, err := dao.TntTenantLevelConfigs.Ctx(ctx).Insert(do.TntTenantLevelConfigs{
Level: req.Level,
Name: req.Name,
CumulativeRechargeThreshold: req.CumulativeRechargeThreshold,
MaxMembers: req.MaxMembers,
MaxConcurrency: req.MaxConcurrency,
PriceMultiplier: req.PriceMultiplier,
SortOrder: req.SortOrder,
})
if err != nil {
return nil, err
}
id, _ := result.LastInsertId()
return &v1.TenantLevelConfigCreateRes{ID: id}, nil
}
func (s *sAdmin) UpdateTenantLevelConfig(ctx context.Context, req *v1.TenantLevelConfigUpdateReq) (*v1.TenantLevelConfigUpdateRes, error) {
_, err := dao.TntTenantLevelConfigs.Ctx(ctx).
Where("id", req.Id).
Data(req.Update).
Update()
if err != nil {
return nil, err
}
return &v1.TenantLevelConfigUpdateRes{}, nil
}
func (s *sAdmin) DeleteTenantLevelConfig(ctx context.Context, req *v1.TenantLevelConfigDeleteReq) (*v1.TenantLevelConfigDeleteRes, error) {
// 不允许删除 level=1(默认等级)
var config entity.TntTenantLevelConfigs
err := dao.TntTenantLevelConfigs.Ctx(ctx).Where("id", req.Id).Scan(&config)
if err != nil {
return nil, err
}
if config.Id == 0 {
return nil, gerror.New("等级配置不存在")
}
if config.Level == 1 {
return nil, gerror.New("不允许删除默认等级 LV1")
}
_, err = dao.TntTenantLevelConfigs.Ctx(ctx).Where("id", req.Id).Delete()
if err != nil {
return nil, err
}
return &v1.TenantLevelConfigDeleteRes{}, nil
}
+118
View File
@@ -0,0 +1,118 @@
package billing
import (
"context"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/model/entity"
)
// GetTenantLevelConfig 获取指定等级的配置
func GetTenantLevelConfig(ctx context.Context, level int) (*entity.TntTenantLevelConfigs, error) {
var config entity.TntTenantLevelConfigs
err := dao.TntTenantLevelConfigs.Ctx(ctx).
Where("level", level).
Scan(&config)
if err != nil {
return nil, err
}
if config.Id == 0 {
return nil, fmt.Errorf("level config not found for level %d", level)
}
return &config, nil
}
// CheckAndUpgradeLevel 检查并升级租户等级(充值后调用)
// 升级策略:仅升不降 — max_members/max_concurrency 仅在新值 > 当前值时才更新
func CheckAndUpgradeLevel(ctx context.Context, tenantID int64) error {
// 1. 读取租户当前等级
var tenant entity.TntTenants
err := dao.TntTenants.Ctx(ctx).Where("id", tenantID).Scan(&tenant)
if err != nil {
return err
}
if tenant.Id == 0 {
return nil
}
// 2. 读取累计充值
var wallet entity.BilWallets
err = dao.BilWallets.Ctx(ctx).Where("tenant_id", tenantID).Scan(&wallet)
if err != nil {
return err
}
if wallet.Id == 0 {
return nil
}
// 3. 根据累计充值计算应得等级
newLevel, err := RecalculateLevel(ctx, wallet.CumulativeRecharge)
if err != nil {
return err
}
// 4. 仅当等级提升时才更新
if newLevel <= tenant.Level {
return nil
}
// 5. 获取新等级配置
config, err := GetTenantLevelConfig(ctx, newLevel)
if err != nil {
return err
}
// 6. 构建更新数据:仅升不降策略
updateData := g.Map{
"level": newLevel,
"updated_at": "NOW()",
}
// max_members:新值 > 当前值才更新(0=无限,视为最大)
if config.MaxMembers == 0 {
// 新等级允许无限成员,直接更新
updateData["max_members"] = 0
} else if config.MaxMembers > tenant.MaxMembers {
updateData["max_members"] = config.MaxMembers
}
// max_concurrency:新值 > 当前值才更新(0=无限,视为最大)
if config.MaxConcurrency == 0 {
updateData["max_concurrency"] = 0
} else if config.MaxConcurrency > tenant.MaxConcurrency {
updateData["max_concurrency"] = config.MaxConcurrency
}
_, err = dao.TntTenants.Ctx(ctx).
Where("id", tenantID).
Data(updateData).
Update()
if err != nil {
return err
}
// 7. 清除并发限制缓存
_, _ = g.Redis().Do(ctx, "DEL", fmt.Sprintf("tenant:conc_limit:%d", tenantID))
return nil
}
// RecalculateLevel 根据累计充值金额重新计算等级
func RecalculateLevel(ctx context.Context, cumulativeRecharge float64) (int, error) {
var config entity.TntTenantLevelConfigs
err := dao.TntTenantLevelConfigs.Ctx(ctx).
Where("cumulative_recharge_threshold <= ?", cumulativeRecharge).
OrderDesc("level").
Limit(1).
Scan(&config)
if err != nil {
return 1, err
}
if config.Id == 0 {
return 1, nil
}
return config.Level, nil
}
+4 -2
View File
@@ -53,6 +53,8 @@ func FulfillOrder(ctx context.Context, orderID int64) error {
if err != nil {
return gerror.Wrapf(err, "credit wallet failed")
}
// 检查并升级租户等级
_ = billing.CheckAndUpgradeLevel(ctx, order.TenantID)
default:
return gerror.Newf("unsupported order type for fulfillment: %s", order.OrderType)
@@ -134,8 +136,8 @@ func creditWallet(ctx context.Context, tenantID int64, amount float64, descripti
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
_, err := tx.Ctx(ctx).Exec(
"UPDATE bil_wallets SET balance = balance + ?, updated_at = NOW() WHERE id = ?",
amount, w.ID)
"UPDATE bil_wallets SET balance = balance + ?, cumulative_recharge = cumulative_recharge + ?, updated_at = NOW() WHERE id = ?",
amount, amount, w.ID)
if err != nil {
return err
}
+6 -4
View File
@@ -80,10 +80,12 @@ func (s *sTenant) Register(ctx context.Context, req *v1.TenantRegisterReq) (*v1.
err = dao.TntTenants.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// Create tenant
tenantResult, err := tx.Model("tnt_tenants").Ctx(ctx).Data(do.TntTenants{
Name: strings.TrimSpace(req.TenantName),
Code: tenantCode,
MaxMembers: 10,
Settings: "{}",
Name: strings.TrimSpace(req.TenantName),
Code: tenantCode,
MaxMembers: 10,
MaxConcurrency: 10,
Level: 1,
Settings: "{}",
}).Insert()
if err != nil {
return gerror.Wrapf(err, "create tenant")
+6 -5
View File
@@ -26,16 +26,17 @@ func (s *sTenant) Wallet(ctx context.Context, req *v1.TenantWalletReq) (*v1.Tena
tenantID := ctxTenantID(ctx)
type walletRow struct {
Balance float64 `json:"balance"`
FrozenBalance float64 `json:"frozen_balance"`
WarningThreshold float64 `json:"warning_threshold"`
Currency string `json:"currency"`
Balance float64 `json:"balance"`
FrozenBalance float64 `json:"frozen_balance"`
WarningThreshold float64 `json:"warning_threshold"`
Currency string `json:"currency"`
CumulativeRecharge float64 `json:"cumulative_recharge"`
}
var w *walletRow
err := dao.BilWallets.Ctx(ctx).
Where("tenant_id", tenantID).
Fields("balance, frozen_balance, warning_threshold, currency").
Fields("balance, frozen_balance, warning_threshold, currency, cumulative_recharge").
Scan(&w)
if err != nil {
return nil, err
+12
View File
@@ -30,6 +30,7 @@ func (s *sTenant) GetOrgInfo(ctx context.Context, req *v1.TenantOrgInfoReq) (*v1
Code string `json:"code"`
LogoUrl string `json:"logo_url"`
Status string `json:"status"`
Level int `json:"level"`
MaxMembers int `json:"max_members"`
CreatedAt string `json:"created_at"`
}
@@ -50,12 +51,23 @@ func (s *sTenant) GetOrgInfo(ctx context.Context, req *v1.TenantOrgInfoReq) (*v1
return nil, err
}
// Look up level name
var levelName string
if tenant.Level > 0 {
_ = dao.TntTenantLevelConfigs.Ctx(ctx).
Where("level", tenant.Level).
Fields("name").
Scan(&levelName)
}
return &v1.TenantOrgInfoRes{
ID: tenant.Id,
Name: tenant.Name,
Code: tenant.Code,
LogoURL: tenant.LogoUrl,
Status: tenant.Status,
Level: tenant.Level,
LevelName: levelName,
MaxMembers: tenant.MaxMembers,
MemberCount: int(memberCount),
CreatedAt: tenant.CreatedAt,
+1 -1
View File
@@ -16,6 +16,7 @@ type AudContentFilterLogs struct {
TenantId any // 租户ID
UserId any // 用户ID
ApiKeyId any // API Key ID
ProjectId any // 项目ID
RequestId any // 请求唯一ID
Method any // HTTP 方法
Path any // 请求路径
@@ -25,5 +26,4 @@ type AudContentFilterLogs struct {
OriginalSnippet any // 原始请求体片段(截断存储,仅 replace 模式)
Blocked any // 是否被拦截(mode=block 时为 true)
CreatedAt *gtime.Time // 创建时间
ProjectId any // 项目ID
}
+10 -9
View File
@@ -11,13 +11,14 @@ import (
// BilWallets is the golang structure of table bil_wallets for DAO operations like Where/Data.
type BilWallets struct {
g.Meta `orm:"table:bil_wallets, do:true"`
Id any // 主键ID
TenantId any // 租户ID(每个租户一个钱包)
Balance any // 总余额
FrozenBalance any // 冻结余额(支付中/退款中,可用余额 = balance - frozen_balance)
WarningThreshold any // 余额预警线(低于此值触发通知)
Currency any // 货币(USD)
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
g.Meta `orm:"table:bil_wallets, do:true"`
Id any // 主键ID
TenantId any // 租户ID(每个租户一个钱包)
Balance any // 总余额
FrozenBalance any // 冻结余额(支付中/退款中,可用余额 = balance - frozen_balance)
WarningThreshold any // 余额预警线(低于此值触发通知)
Currency any // 货币(USD)
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
CumulativeRecharge any // 累计充值总额(USD)
}
+1 -1
View File
@@ -20,7 +20,7 @@ type ChnChannelKeys struct {
LastUsedAt *gtime.Time // 最后使用时间
LastError any // 最后一次错误信息
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
KeyType any // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)
TokenExpiresAt *gtime.Time // OAuth access_token 过期时间(仅 key_type=oauth 时有值)
UpdatedAt *gtime.Time // 更新时间
}
+1 -1
View File
@@ -21,10 +21,10 @@ type MdlModels struct {
MaxOutputTokens any // 最大输出 token 数
Description any // 模型描述
Tags []string // 标签(如 reasoning、vision、function_calling)
Capabilities any // 模型能力特性(如 vision、function_calling、reasoning 等)
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
DeprecatedAt *gtime.Time // 标记弃用的时间(NULL表示未弃用)
SunsetDate *gtime.Time // 计划下线日期(到达后返回410 Gone,NULL表示未设置)
ReplacementModel any // 推荐替代模型名(NULL表示无替代)
Capabilities any // 模型能力特性(如 vision、function_calling、reasoning 等)
}
+1 -1
View File
@@ -22,6 +22,6 @@ type NtfMessages struct {
IsRead any // 是否已读:0=未读, 1=已读
IsBroadcast any // 是否广播消息:0=个人消息, 1=广播消息
Metadata any // 附加元数据(JSONB,如关联资源ID、跳转链接等)
CreatedAt *gtime.Time // 创建时间
TargetRoles any // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
CreatedAt *gtime.Time // 创建时间
}
@@ -0,0 +1,25 @@
// =================================================================================
// 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"
)
// TntTenantLevelConfigs is the golang structure of table tnt_tenant_level_configs for DAO operations like Where/Data.
type TntTenantLevelConfigs struct {
g.Meta `orm:"table:tnt_tenant_level_configs, do:true"`
Id any //
Level any // 等级号(1, 2, 3...)
Name any // 等级名称
CumulativeRechargeThreshold any // 累计充值阈值(USD),达到此值自动升级
MaxMembers any // 该等级最大成员数
MaxConcurrency any // 该等级最大并发数,0=无限
PriceMultiplier any // 价格乘数(折扣,如 0.9=九折)
SortOrder any // 排序权重
CreatedAt *gtime.Time //
UpdatedAt *gtime.Time //
}
+1
View File
@@ -29,4 +29,5 @@ type TntTenants struct {
DataRemovalAt *gtime.Time // 数据清除时间(冻结 30 天后)
MaxConcurrency any // 租户总并发上限(0表示不限制)
DefaultChannelScope any // 默认渠道范围(NULL或[]表示全部可用,否则为channel_id数组)
Level any // 当前等级(对应 tnt_tenant_level_configs.level)
}
+24 -24
View File
@@ -12,29 +12,29 @@ import (
// TskModelTasks is the golang structure of table tsk_model_tasks for DAO operations like Where/Data.
type TskModelTasks struct {
g.Meta `orm:"table:tsk_model_tasks, do:true"`
Id any //
PublicTaskId any //
Platform any //
Action any //
Status any //
Progress any //
FailReason any //
TenantId any //
UserId any //
ApiKeyId any //
ChannelId any //
ModelName any //
UpstreamModel any //
PreDeductAmount any //
ActualCost any //
BillingSettled any //
ResultUrl any //
Data any //
PrivateData any //
SubmitTime *gtime.Time //
StartTime *gtime.Time //
FinishTime *gtime.Time //
CreatedAt *gtime.Time //
UpdatedAt *gtime.Time //
Id any // 主键ID
PublicTaskId any // 对外公开的任务ID(task_xxxxx 格式),用于 API 响应和客户端查询
Platform any // 任务所属平台:sora、kling、suno、midjourney、volcengine
Action any // 任务动作类型。通用:generate;Suno:music、lyrics;
Status any // 任务状态:NOT_START-未开始、SUBMITTED-已提交、IN_PROGRESS-进行中、SUCCESS-成功、FAILURE-失败
Progress any // 任务进度,字符串格式百分比(如 0%、50%、100%)
FailReason any // 任务失败原因文本
TenantId any // 发起任务的租户ID
UserId any // 发起任务的租户用户ID
ApiKeyId any // 调用方使用的 API Key ID
ChannelId any // 转发请求的渠道ID
ModelName any // 用户请求的模型名称(如 sora-1.0-turbo、midjourney 等)
UpstreamModel any // 上游供应商实际使用的模型名称,可能与请求模型不同
PreDeductAmount any // 提交任务时的预扣金额(USD),任务完成后根据实际用量结算差额
ActualCost any // 任务实际消费金额(USD),成功时按此结算,失败时退还预扣金额
BillingSettled any // 计费是否已结算(true 表示已完成预扣与实际金额的多退少补)
ResultUrl any // 任务完成后的结果资源 URL(如生成的视频/图片/音频的下载地址)
Data any // 上游供应商返回的完整响应数据(JSONB),可返回给用户查看
PrivateData any // 内部私有数据(JSONB),含 upstream_task_id 等敏感字段,用于轮询上游状态,不返回给用户
SubmitTime *gtime.Time // 任务提交到上游供应商的时间
StartTime *gtime.Time // 上游供应商开始执行任务的时间
FinishTime *gtime.Time // 任务完成(成功或失败)的时间
CreatedAt *gtime.Time // 记录创建时间
UpdatedAt *gtime.Time // 记录更新时间
RequestId any // 任务提交时的原始请求 ID(req_xxxxx),关联 aud_request_logs.request_id
}
@@ -14,6 +14,7 @@ type AudContentFilterLogs struct {
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID
UserId int64 `json:"user_id" orm:"user_id" description:"用户ID"` // 用户ID
ApiKeyId int64 `json:"api_key_id" orm:"api_key_id" description:"API Key ID"` // API Key ID
ProjectId int64 `json:"project_id" orm:"project_id" description:"项目ID"` // 项目ID
RequestId string `json:"request_id" orm:"request_id" description:"请求唯一ID"` // 请求唯一ID
Method string `json:"method" orm:"method" description:"HTTP 方法"` // HTTP 方法
Path string `json:"path" orm:"path" description:"请求路径"` // 请求路径
@@ -23,5 +24,4 @@ type AudContentFilterLogs struct {
OriginalSnippet string `json:"original_snippet" orm:"original_snippet" description:"原始请求体片段(截断存储,仅 replace 模式)"` // 原始请求体片段(截断存储,仅 replace 模式)
Blocked bool `json:"blocked" orm:"blocked" description:"是否被拦截(mode=block 时为 true)"` // 是否被拦截(mode=block 时为 true)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
ProjectId int64 `json:"project_id" orm:"project_id" description:"项目ID"` // 项目ID
}
+18 -18
View File
@@ -10,22 +10,22 @@ import (
// BilTransactions is the golang structure for table bil_transactions.
type BilTransactions struct {
Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID
WalletId int64 `json:"wallet_id" orm:"wallet_id" description:"关联钱包ID"` // 关联钱包ID
Type string `json:"type" orm:"type" description:"类型:consume(消费)/ recharge(充值)/ adjust(调整)/ pre_deduct(预扣,已废弃)/ settle(结算,已废弃)/ refund(退款,已废弃)/ freeze(冻结,已废弃)/ unfreeze(解冻,已废弃)"` // 类型:consume(消费)/ recharge(充值)/ adjust(调整)/ pre_deduct(预扣,已废弃)/ settle(结算,已废弃)/ refund(退款,已废弃)/ freeze(冻结,已废弃)/ unfreeze(解冻,已废弃)
Amount float64 `json:"amount" orm:"amount" description:"变动金额(正数=收入,负数=支出)"` // 变动金额(正数=收入,负数=支出)
BalanceAfter float64 `json:"balance_after" orm:"balance_after" description:"变动后总余额"` // 变动后总余额
FrozenAfter float64 `json:"frozen_after" orm:"frozen_after" description:"变动后冻结余额"` // 变动后冻结余额
RelatedId int64 `json:"related_id" orm:"related_id" description:"关联业务ID(如计费记录ID、订单ID等)"` // 关联业务ID(如计费记录ID、订单ID等)
RelatedType string `json:"related_type" orm:"related_type" description:"关联业务类型:billing_record / order / refund / adjustment / redemption"` // 关联业务类型:billing_record / order / refund / adjustment / redemption
Description string `json:"description" orm:"description" description:"交易描述"` // 交易描述
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
UserId int64 `json:"user_id" orm:"user_id" description:"关联用户ID(consume 类型为实际消费用户,recharge 类型为操作用户,adjust 类型为空)"` // 关联用户ID(consume 类型为实际消费用户,recharge 类型为操作用户,adjust 类型为空)
RequestId string `json:"request_id" orm:"request_id" description:"关联请求ID(consume 类型对应 API 调用的 request_id,其他类型为空)"` // 关联请求ID(consume 类型对应 API 调用的 request_id,其他类型为空)
ModelName string `json:"model_name" orm:"model_name" description:"关联模型名(consume 类型为调用的模型名,其他类型为空)"` // 关联模型名(consume 类型为调用的模型名,其他类型为空)
ProjectId int64 `json:"project_id" orm:"project_id" description:"关联项目ID(consume 类型为 API Key 所属项目,个人密钥为空)"` // 关联项目ID(consume 类型为 API Key 所属项目,个人密钥为空)
ApiKeyId int64 `json:"api_key_id" orm:"api_key_id" description:"关联API密钥ID(consume 类型为发起请求的密钥)"` // 关联API密钥ID(consume 类型为发起请求的密钥)
TaskId string `json:"task_id" orm:"task_id" description:"关联异步任务公开ID(consume+relay_mode=task 时关联 tsk_model_tasks.public_task_id)"` // 关联异步任务公开ID(consume+relay_mode=task 时关联 tsk_model_tasks.public_task_id)
Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID
WalletId int64 `json:"wallet_id" orm:"wallet_id" description:"关联钱包ID"` // 关联钱包ID
Type string `json:"type" orm:"type" description:"类型:consume(消费)/ recharge(充值)/ adjust(调整)/ pre_deduct(预扣,已废弃)/ settle(结算,已废弃)/ refund(退款,已废弃)/ freeze(冻结,已废弃)/ unfreeze(解冻,已废弃)"` // 类型:consume(消费)/ recharge(充值)/ adjust(调整)/ pre_deduct(预扣,已废弃)/ settle(结算,已废弃)/ refund(退款,已废弃)/ freeze(冻结,已废弃)/ unfreeze(解冻,已废弃)
Amount float64 `json:"amount" orm:"amount" description:"变动金额(正数=收入,负数=支出)"` // 变动金额(正数=收入,负数=支出)
BalanceAfter float64 `json:"balance_after" orm:"balance_after" description:"变动后总余额"` // 变动后总余额
FrozenAfter float64 `json:"frozen_after" orm:"frozen_after" description:"变动后冻结余额"` // 变动后冻结余额
RelatedId int64 `json:"related_id" orm:"related_id" description:"关联业务ID(如计费记录ID、订单ID等)"` // 关联业务ID(如计费记录ID、订单ID等)
RelatedType string `json:"related_type" orm:"related_type" description:"关联业务类型:billing_record / order / refund / adjustment / redemption"` // 关联业务类型:billing_record / order / refund / adjustment / redemption
Description string `json:"description" orm:"description" description:"交易描述"` // 交易描述
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
UserId int64 `json:"user_id" orm:"user_id" description:"关联用户ID(consume 类型为实际消费用户,recharge 类型为操作用户,adjust 类型为空)"` // 关联用户ID(consume 类型为实际消费用户,recharge 类型为操作用户,adjust 类型为空)
RequestId string `json:"request_id" orm:"request_id" description:"关联请求ID(consume 类型对应 API 调用的 request_id,其他类型为空)"` // 关联请求ID(consume 类型对应 API 调用的 request_id,其他类型为空)
ModelName string `json:"model_name" orm:"model_name" description:"关联模型名(consume 类型为调用的模型名,其他类型为空)"` // 关联模型名(consume 类型为调用的模型名,其他类型为空)
ProjectId int64 `json:"project_id" orm:"project_id" description:"关联项目ID(consume 类型为 API Key 所属项目,个人密钥为空)"` // 关联项目ID(consume 类型为 API Key 所属项目,个人密钥为空)
ApiKeyId int64 `json:"api_key_id" orm:"api_key_id" description:"关联API密钥ID(consume 类型为发起请求的密钥)"` // 关联API密钥ID(consume 类型为发起请求的密钥)
TaskId string `json:"task_id" orm:"task_id" description:"关联异步任务公开ID(consume+relay_mode=task 时关联 tsk_model_tasks.public_task_id)"` // 关联异步任务公开ID(consume+relay_mode=task 时关联 tsk_model_tasks.public_task_id)
}
+9 -8
View File
@@ -10,12 +10,13 @@ import (
// BilWallets is the golang structure for table bil_wallets.
type BilWallets struct {
Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID(每个租户一个钱包)"` // 租户ID(每个租户一个钱包)
Balance float64 `json:"balance" orm:"balance" description:"总余额"` // 总余额
FrozenBalance float64 `json:"frozen_balance" orm:"frozen_balance" description:"冻结余额(支付中/退款中,可用余额 = balance - frozen_balance)"` // 冻结余额(支付中/退款中,可用余额 = balance - frozen_balance)
WarningThreshold float64 `json:"warning_threshold" orm:"warning_threshold" description:"余额预警线(低于此值触发通知)"` // 余额预警线(低于此值触发通知)
Currency string `json:"currency" orm:"currency" description:"货币(USD)"` // 货币(USD)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID(每个租户一个钱包)"` // 租户ID(每个租户一个钱包)
Balance float64 `json:"balance" orm:"balance" description:"总余额"` // 总余额
FrozenBalance float64 `json:"frozen_balance" orm:"frozen_balance" description:"冻结余额(支付中/退款中,可用余额 = balance - frozen_balance)"` // 冻结余额(支付中/退款中,可用余额 = balance - frozen_balance)
WarningThreshold float64 `json:"warning_threshold" orm:"warning_threshold" description:"余额预警线(低于此值触发通知)"` // 余额预警线(低于此值触发通知)
Currency string `json:"currency" orm:"currency" description:"货币(USD)"` // 货币(USD)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
CumulativeRecharge float64 `json:"cumulative_recharge" orm:"cumulative_recharge" description:"累计充值总额(USD)"` // 累计充值总额(USD)
}
+1 -1
View File
@@ -18,7 +18,7 @@ type ChnChannelKeys struct {
LastUsedAt *gtime.Time `json:"last_used_at" orm:"last_used_at" description:"最后使用时间"` // 最后使用时间
LastError string `json:"last_error" orm:"last_error" description:"最后一次错误信息"` // 最后一次错误信息
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
KeyType string `json:"key_type" orm:"key_type" description:"Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)"` // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)
TokenExpiresAt *gtime.Time `json:"token_expires_at" orm:"token_expires_at" description:"OAuth access_token 过期时间(仅 key_type=oauth 时有值)"` // OAuth access_token 过期时间(仅 key_type=oauth 时有值)
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
}
+1 -1
View File
@@ -19,10 +19,10 @@ type MdlModels struct {
MaxOutputTokens int `json:"max_output_tokens" orm:"max_output_tokens" description:"最大输出 token 数"` // 最大输出 token 数
Description string `json:"description" orm:"description" description:"模型描述"` // 模型描述
Tags []string `json:"tags" orm:"tags" description:"标签(如 reasoning、vision、function_calling)"` // 标签(如 reasoning、vision、function_calling)
Capabilities string `json:"capabilities" orm:"capabilities" description:"模型能力特性(如 vision、function_calling、reasoning 等)"` // 模型能力特性(如 vision、function_calling、reasoning 等)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
DeprecatedAt *gtime.Time `json:"deprecated_at" orm:"deprecated_at" description:"标记弃用的时间(NULL表示未弃用)"` // 标记弃用的时间(NULL表示未弃用)
SunsetDate *gtime.Time `json:"sunset_date" orm:"sunset_date" description:"计划下线日期(到达后返回410 Gone,NULL表示未设置)"` // 计划下线日期(到达后返回410 Gone,NULL表示未设置)
ReplacementModel string `json:"replacement_model" orm:"replacement_model" description:"推荐替代模型名(NULL表示无替代)"` // 推荐替代模型名(NULL表示无替代)
Capabilities string `json:"capabilities" orm:"capabilities" description:"模型能力特性(如 vision、function_calling、reasoning 等)"` // 模型能力特性(如 vision、function_calling、reasoning 等)
}
+1 -1
View File
@@ -20,6 +20,6 @@ type NtfMessages struct {
IsRead int `json:"is_read" orm:"is_read" description:"是否已读:0=未读, 1=已读"` // 是否已读:0=未读, 1=已读
IsBroadcast int `json:"is_broadcast" orm:"is_broadcast" description:"是否广播消息:0=个人消息, 1=广播消息"` // 是否广播消息:0=个人消息, 1=广播消息
Metadata string `json:"metadata" orm:"metadata" description:"附加元数据(JSONB,如关联资源ID、跳转链接等)"` // 附加元数据(JSONB,如关联资源ID、跳转链接等)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
TargetRoles string `json:"target_roles" orm:"target_roles" description:"目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)"` // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
}
@@ -0,0 +1,23 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// TntTenantLevelConfigs is the golang structure for table tnt_tenant_level_configs.
type TntTenantLevelConfigs struct {
Id int64 `json:"id" orm:"id" description:""` //
Level int `json:"level" orm:"level" description:"等级号(1, 2, 3...)"` // 等级号(1, 2, 3...)
Name string `json:"name" orm:"name" description:"等级名称"` // 等级名称
CumulativeRechargeThreshold float64 `json:"cumulative_recharge_threshold" orm:"cumulative_recharge_threshold" description:"累计充值阈值(USD),达到此值自动升级"` // 累计充值阈值(USD),达到此值自动升级
MaxMembers int `json:"max_members" orm:"max_members" description:"该等级最大成员数"` // 该等级最大成员数
MaxConcurrency int `json:"max_concurrency" orm:"max_concurrency" description:"该等级最大并发数,0=无限"` // 该等级最大并发数,0=无限
PriceMultiplier float64 `json:"price_multiplier" orm:"price_multiplier" description:"价格乘数(折扣,如 0.9=九折)"` // 价格乘数(折扣,如 0.9=九折)
SortOrder int `json:"sort_order" orm:"sort_order" description:"排序权重"` // 排序权重
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` //
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` //
}
+1
View File
@@ -27,4 +27,5 @@ type TntTenants struct {
DataRemovalAt *gtime.Time `json:"data_removal_at" orm:"data_removal_at" description:"数据清除时间(冻结 30 天后)"` // 数据清除时间(冻结 30 天后)
MaxConcurrency int `json:"max_concurrency" orm:"max_concurrency" description:"租户总并发上限(0表示不限制)"` // 租户总并发上限(0表示不限制)
DefaultChannelScope string `json:"default_channel_scope" orm:"default_channel_scope" description:"默认渠道范围(NULL或[]表示全部可用,否则为channel_id数组)"` // 默认渠道范围(NULL或[]表示全部可用,否则为channel_id数组)
Level int `json:"level" orm:"level" description:"当前等级(对应 tnt_tenant_level_configs.level)"` // 当前等级(对应 tnt_tenant_level_configs.level)
}
+25 -25
View File
@@ -10,29 +10,29 @@ import (
// TskModelTasks is the golang structure for table tsk_model_tasks.
type TskModelTasks struct {
Id int64 `json:"id" orm:"id" description:""` //
PublicTaskId string `json:"public_task_id" orm:"public_task_id" description:""` //
Platform string `json:"platform" orm:"platform" description:""` //
Action string `json:"action" orm:"action" description:""` //
Status string `json:"status" orm:"status" description:""` //
Progress string `json:"progress" orm:"progress" description:""` //
FailReason string `json:"fail_reason" orm:"fail_reason" description:""` //
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:""` //
UserId int64 `json:"user_id" orm:"user_id" description:""` //
ApiKeyId int64 `json:"api_key_id" orm:"api_key_id" description:""` //
ChannelId int64 `json:"channel_id" orm:"channel_id" description:""` //
ModelName string `json:"model_name" orm:"model_name" description:""` //
UpstreamModel string `json:"upstream_model" orm:"upstream_model" description:""` //
PreDeductAmount float64 `json:"pre_deduct_amount" orm:"pre_deduct_amount" description:""` //
ActualCost float64 `json:"actual_cost" orm:"actual_cost" description:""` //
BillingSettled bool `json:"billing_settled" orm:"billing_settled" description:""` //
ResultUrl string `json:"result_url" orm:"result_url" description:""` //
Data string `json:"data" orm:"data" description:""` //
PrivateData string `json:"private_data" orm:"private_data" description:""` //
SubmitTime *gtime.Time `json:"submit_time" orm:"submit_time" description:""` //
StartTime *gtime.Time `json:"start_time" orm:"start_time" description:""` //
FinishTime *gtime.Time `json:"finish_time" orm:"finish_time" description:""` //
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` //
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` //
RequestId string `json:"request_id" orm:"request_id" description:"任务提交时的原始请求 ID(req_xxxxx),关联 aud_request_logs.request_id"` // 任务提交时的原始请求 ID(req_xxxxx),关联 aud_request_logs.request_id
Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID
PublicTaskId string `json:"public_task_id" orm:"public_task_id" description:"对外公开的任务ID(task_xxxxx 格式),用于 API 响应和客户端查询"` // 对外公开的任务ID(task_xxxxx 格式),用于 API 响应和客户端查询
Platform string `json:"platform" orm:"platform" description:"任务所属平台:sora、kling、suno、midjourney、volcengine"` // 任务所属平台:sora、kling、suno、midjourney、volcengine
Action string `json:"action" orm:"action" description:"任务动作类型。通用:generate;Suno:music、lyrics;"` // 任务动作类型。通用:generate;Suno:music、lyrics;
Status string `json:"status" orm:"status" description:"任务状态:NOT_START-未开始、SUBMITTED-已提交、IN_PROGRESS-进行中、SUCCESS-成功、FAILURE-失败"` // 任务状态:NOT_START-未开始、SUBMITTED-已提交、IN_PROGRESS-进行中、SUCCESS-成功、FAILURE-失败
Progress string `json:"progress" orm:"progress" description:"任务进度,字符串格式百分比(如 0%、50%、100%)"` // 任务进度,字符串格式百分比(如 0%、50%、100%)
FailReason string `json:"fail_reason" orm:"fail_reason" description:"任务失败原因文本"` // 任务失败原因文本
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"发起任务的租户ID"` // 发起任务的租户ID
UserId int64 `json:"user_id" orm:"user_id" description:"发起任务的租户用户ID"` // 发起任务的租户用户ID
ApiKeyId int64 `json:"api_key_id" orm:"api_key_id" description:"调用方使用的 API Key ID"` // 调用方使用的 API Key ID
ChannelId int64 `json:"channel_id" orm:"channel_id" description:"转发请求的渠道ID"` // 转发请求的渠道ID
ModelName string `json:"model_name" orm:"model_name" description:"用户请求的模型名称(如 sora-1.0-turbo、midjourney 等)"` // 用户请求的模型名称(如 sora-1.0-turbo、midjourney 等)
UpstreamModel string `json:"upstream_model" orm:"upstream_model" description:"上游供应商实际使用的模型名称,可能与请求模型不同"` // 上游供应商实际使用的模型名称,可能与请求模型不同
PreDeductAmount float64 `json:"pre_deduct_amount" orm:"pre_deduct_amount" description:"提交任务时的预扣金额(USD),任务完成后根据实际用量结算差额"` // 提交任务时的预扣金额(USD),任务完成后根据实际用量结算差额
ActualCost float64 `json:"actual_cost" orm:"actual_cost" description:"任务实际消费金额(USD),成功时按此结算,失败时退还预扣金额"` // 任务实际消费金额(USD),成功时按此结算,失败时退还预扣金额
BillingSettled bool `json:"billing_settled" orm:"billing_settled" description:"计费是否已结算(true 表示已完成预扣与实际金额的多退少补)"` // 计费是否已结算(true 表示已完成预扣与实际金额的多退少补)
ResultUrl string `json:"result_url" orm:"result_url" description:"任务完成后的结果资源 URL(如生成的视频/图片/音频的下载地址)"` // 任务完成后的结果资源 URL(如生成的视频/图片/音频的下载地址)
Data string `json:"data" orm:"data" description:"上游供应商返回的完整响应数据(JSONB),可返回给用户查看"` // 上游供应商返回的完整响应数据(JSONB),可返回给用户查看
PrivateData string `json:"private_data" orm:"private_data" description:"内部私有数据(JSONB),含 upstream_task_id 等敏感字段,用于轮询上游状态,不返回给用户"` // 内部私有数据(JSONB),含 upstream_task_id 等敏感字段,用于轮询上游状态,不返回给用户
SubmitTime *gtime.Time `json:"submit_time" orm:"submit_time" description:"任务提交到上游供应商的时间"` // 任务提交到上游供应商的时间
StartTime *gtime.Time `json:"start_time" orm:"start_time" description:"上游供应商开始执行任务的时间"` // 上游供应商开始执行任务的时间
FinishTime *gtime.Time `json:"finish_time" orm:"finish_time" description:"任务完成(成功或失败)的时间"` // 任务完成(成功或失败)的时间
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"记录创建时间"` // 记录创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"记录更新时间"` // 记录更新时间
RequestId string `json:"request_id" orm:"request_id" description:"任务提交时的原始请求 ID(req_xxxxx),关联 aud_request_logs.request_id"` // 任务提交时的原始请求 ID(req_xxxxx),关联 aud_request_logs.request_id
}
+4
View File
@@ -363,6 +363,10 @@ type (
UpdateTenantChannelScope(ctx context.Context, req *v1.TenantChannelScopeUpdateReq) (*v1.TenantChannelScopeUpdateRes, error)
// ExportTenants exports tenant list to CSV or Excel.
ExportTenants(ctx context.Context, req *v1.TenantExportReq) (*v1.TenantExportRes, error)
ListTenantLevelConfigs(ctx context.Context, _ *v1.TenantLevelConfigListReq) (*v1.TenantLevelConfigListRes, error)
CreateTenantLevelConfig(ctx context.Context, req *v1.TenantLevelConfigCreateReq) (*v1.TenantLevelConfigCreateRes, error)
UpdateTenantLevelConfig(ctx context.Context, req *v1.TenantLevelConfigUpdateReq) (*v1.TenantLevelConfigUpdateRes, error)
DeleteTenantLevelConfig(ctx context.Context, req *v1.TenantLevelConfigDeleteReq) (*v1.TenantLevelConfigDeleteRes, error)
// ListTenantModels 列出租户已分配的模型
ListTenantModels(ctx context.Context, req *v1.TenantModelListReq) (*v1.TenantModelListRes, error)
// BatchAssignModels 批量分配模型给租户
+47
View File
@@ -0,0 +1,47 @@
-- +goose Up
-- 租户等级系统:基于累计充值的自动等级体系
-- 1. 新建等级配置表
CREATE TABLE tnt_tenant_level_configs (
id BIGSERIAL PRIMARY KEY,
level INT NOT NULL,
name VARCHAR(32) NOT NULL,
cumulative_recharge_threshold NUMERIC(20,10) NOT NULL,
max_members INT NOT NULL DEFAULT 5,
max_concurrency INT NOT NULL DEFAULT 10,
price_multiplier NUMERIC(5,4) NOT NULL DEFAULT 1.0000,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT uk_tnt_tenant_level_configs_level UNIQUE (level)
);
COMMENT ON TABLE tnt_tenant_level_configs IS '租户等级配置表';
COMMENT ON COLUMN tnt_tenant_level_configs.level IS '等级号(1, 2, 3...)';
COMMENT ON COLUMN tnt_tenant_level_configs.name IS '等级名称';
COMMENT ON COLUMN tnt_tenant_level_configs.cumulative_recharge_threshold IS '累计充值阈值(USD),达到此值自动升级';
COMMENT ON COLUMN tnt_tenant_level_configs.max_members IS '该等级最大成员数';
COMMENT ON COLUMN tnt_tenant_level_configs.max_concurrency IS '该等级最大并发数,0=无限';
COMMENT ON COLUMN tnt_tenant_level_configs.price_multiplier IS '价格乘数(折扣,如 0.9=九折)';
COMMENT ON COLUMN tnt_tenant_level_configs.sort_order IS '排序权重';
-- 2. 插入默认等级数据
INSERT INTO tnt_tenant_level_configs (level, name, cumulative_recharge_threshold, max_members, max_concurrency, price_multiplier, sort_order) VALUES
(1, 'LV1', 0, 5, 10, 1.0000, 1),
(2, 'LV2', 100, 10, 20, 0.9500, 2),
(3, 'LV3', 500, 20, 50, 0.9000, 3),
(4, 'LV4', 2000, 50, 100, 0.8500, 4),
(5, 'LV5', 10000, 0, 0, 0.8000, 5);
-- 3. bil_wallets 添加累计充值字段
ALTER TABLE bil_wallets ADD COLUMN cumulative_recharge NUMERIC(20,10) DEFAULT 0 NOT NULL;
COMMENT ON COLUMN bil_wallets.cumulative_recharge IS '累计充值总额(USD)';
-- 4. tnt_tenants 添加等级字段
ALTER TABLE tnt_tenants ADD COLUMN level INT DEFAULT 1 NOT NULL;
COMMENT ON COLUMN tnt_tenants.level IS '当前等级(对应 tnt_tenant_level_configs.level)';
-- +goose Down
ALTER TABLE tnt_tenants DROP COLUMN IF EXISTS level;
ALTER TABLE bil_wallets DROP COLUMN IF EXISTS cumulative_recharge;
DROP TABLE IF EXISTS tnt_tenant_level_configs;
+6
View File
@@ -150,6 +150,12 @@ const adminRoutes: RouteRecordRaw[] = [
component: () => import('@/views/tenant/TenantDetailPage.vue'),
meta: { title: '租户详情' },
},
{
path: 'tenant-levels',
name: 'AdminTenantLevels',
component: () => import('@/views/tenant/TenantLevelsPage.vue'),
meta: { title: '租户级别' },
},
{
path: 'members',
name: 'AdminMembers',
+1
View File
@@ -163,6 +163,7 @@ const menuGroups = [
icon: IconUserGroup,
items: [
{ name: 'AdminTenants', label: '租户列表', icon: IconHome },
{ name: 'AdminTenantLevels', label: '租户级别', icon: IconLayers },
{ name: 'AdminMembers', label: '成员列表', icon: IconUser },
{ name: 'AdminUsageLogs', label: '用量日志', icon: IconFile },
],
@@ -63,6 +63,7 @@ function startEdit() {
editForm.name = detail.value.name || ''
editForm.max_members = detail.value.max_members || null
editForm.max_concurrency = detail.value.max_concurrency || null
editForm.level = detail.value.level || null
}
async function saveEdit() {
@@ -497,6 +498,9 @@ onMounted(() => {
<AFormItem label="并发上限">
<AInputNumber v-model="editForm.max_concurrency" :min="0" placeholder="0=不限" style="width: 120px" />
</AFormItem>
<AFormItem label="等级">
<AInputNumber v-model="editForm.level" :min="1" :max="99" placeholder="等级号" style="width: 120px" />
</AFormItem>
<AFormItem>
<AButton type="primary" :loading="editLoading" @click="saveEdit">保存</AButton>
</AFormItem>
@@ -0,0 +1,182 @@
<script setup lang="ts">
import { ref, reactive, onMounted, h } from 'vue'
import {
Tag, Button, Space, Message, Modal,
} from '@arco-design/web-vue'
import type { TableColumnData, FormInstance } from '@arco-design/web-vue'
import PageHeader from '@/components/PageHeader.vue'
import request from '@/utils/request'
const loading = ref(false)
const levels = ref<any[]>([])
const columns: TableColumnData[] = [
{ title: '等级号', dataIndex: 'level', width: 80 },
{ title: '名称', dataIndex: 'name', width: 120 },
{
title: '累计充值阈值(USD)', dataIndex: 'cumulative_recharge_threshold', width: 180,
render({ record }) { return `$${Number(record.cumulative_recharge_threshold).toFixed(2)}` },
},
{
title: '最大成员数', dataIndex: 'max_members', width: 100,
render({ record }) { return record.max_members === 0 ? '不限' : record.max_members },
},
{
title: '最大并发数', dataIndex: 'max_concurrency', width: 100,
render({ record }) { return record.max_concurrency === 0 ? '不限' : record.max_concurrency },
},
{
title: '价格乘数', dataIndex: 'price_multiplier', width: 100,
render({ record }) {
const v = Number(record.price_multiplier)
if (v === 1) return '-'
return `${(v * 100).toFixed(1)}%`
},
},
{ title: '排序', dataIndex: 'sort_order', width: 70 },
{
title: '操作', dataIndex: 'actions', width: 150, fixed: 'right',
render({ record }) {
return h(Space, { size: 4 }, () => [
h(Button, { size: 'small', type: 'primary', onClick: () => openEditModal(record) }, () => '编辑'),
record.level === 1 ? null : h(Button, { size: 'small', status: 'danger', onClick: () => deleteLevel(record) }, () => '删除'),
])
},
},
]
async function fetchLevels() {
loading.value = true
try {
const res = await request.get('/admin/tenant-level-configs')
levels.value = res.data?.data?.list || []
} catch { /* interceptor handles error toast */ } finally { loading.value = false }
}
const showModal = ref(false)
const editingId = ref<number | null>(null)
const formRef = ref<FormInstance | null>(null)
const formLoading = ref(false)
const formData = reactive({
level: 1,
name: '',
cumulative_recharge_threshold: 0,
max_members: 5,
max_concurrency: 10,
price_multiplier: 1.0,
sort_order: 0,
})
function resetForm() {
formData.level = 1
formData.name = ''
formData.cumulative_recharge_threshold = 0
formData.max_members = 5
formData.max_concurrency = 10
formData.price_multiplier = 1.0
formData.sort_order = 0
}
function openCreateModal() {
editingId.value = null
resetForm()
showModal.value = true
}
function openEditModal(row: any) {
editingId.value = row.id
formData.level = row.level
formData.name = row.name
formData.cumulative_recharge_threshold = row.cumulative_recharge_threshold
formData.max_members = row.max_members
formData.max_concurrency = row.max_concurrency
formData.price_multiplier = row.price_multiplier
formData.sort_order = row.sort_order
showModal.value = true
}
async function handleSubmit() {
formLoading.value = true
try {
if (editingId.value) {
await request.put(`/admin/tenant-level-configs/${editingId.value}`, {
update: {
name: formData.name,
cumulative_recharge_threshold: formData.cumulative_recharge_threshold,
max_members: formData.max_members,
max_concurrency: formData.max_concurrency,
price_multiplier: formData.price_multiplier,
sort_order: formData.sort_order,
},
})
Message.success('更新成功')
} else {
await request.post('/admin/tenant-level-configs', formData)
Message.success('创建成功')
}
showModal.value = false
fetchLevels()
} catch { /* interceptor handles error toast */ } finally { formLoading.value = false }
}
function deleteLevel(row: any) {
Modal.warning({
title: '确认删除',
content: `确定要删除等级「${row.name}」吗?`,
okText: '确认', cancelText: '取消',
onOk: async () => {
try { await request.delete(`/admin/tenant-level-configs/${row.id}`); Message.success('已删除'); fetchLevels() }
catch { /* interceptor handles error toast */ }
},
})
}
onMounted(fetchLevels)
</script>
<template>
<div>
<PageHeader title="租户级别" description="配置租户等级体系,基于累计充值自动升级">
<template #actions>
<AButton type="primary" @click="openCreateModal">创建等级</AButton>
</template>
</PageHeader>
<ACard :bordered="false">
<template #title>等级配置列表</template>
<ATable :columns="columns" :data="levels" :loading="loading" row-key="id" :scroll="{ x: 900 }" :pagination="false" />
</ACard>
<ADrawer v-model:visible="showModal" :title="editingId ? '编辑等级' : '创建等级'" :width="520" :mask-closable="false" :footer="true" unmount-on-close>
<AForm ref="formRef" :model="formData" :auto-label-width="true" layout="vertical">
<AFormItem label="等级号" required>
<AInputNumber v-model="formData.level" :min="1" :max="99" class="w-full" :disabled="!!editingId" placeholder="1, 2, 3..." />
</AFormItem>
<AFormItem label="等级名称" required>
<AInput v-model="formData.name" placeholder="如:LV2 白银用户" />
</AFormItem>
<AFormItem label="累计充值阈值(USD)" required>
<AInputNumber v-model="formData.cumulative_recharge_threshold" :min="0" :precision="2" class="w-full" placeholder="达到此金额自动升级" />
</AFormItem>
<AFormItem label="最大成员数">
<AInputNumber v-model="formData.max_members" :min="0" class="w-full" placeholder="0 = 不限制" />
</AFormItem>
<AFormItem label="最大并发数">
<AInputNumber v-model="formData.max_concurrency" :min="0" class="w-full" placeholder="0 = 不限制" />
</AFormItem>
<AFormItem label="价格乘数">
<AInputNumber v-model="formData.price_multiplier" :min="0.01" :max="2" :step="0.05" :precision="4" class="w-full" placeholder="1.0 = 原价,0.9 = 九折" />
</AFormItem>
<AFormItem label="排序权重">
<AInputNumber v-model="formData.sort_order" :min="0" class="w-full" />
</AFormItem>
</AForm>
<template #footer>
<Space>
<AButton @click="showModal = false">取消</AButton>
<AButton type="primary" :loading="formLoading" @click="handleSubmit">确认</AButton>
</Space>
</template>
</ADrawer>
</div>
</template>
@@ -214,6 +214,14 @@ onMounted(() => {
</span>
</div>
<!-- Level -->
<div>
<p class="input-label">等级</p>
<span class="badge badge-primary">
{{ orgInfo.level_name || 'LV' + orgInfo.level }}
</span>
</div>
<!-- Member Count -->
<div>
<p class="input-label">成员数</p>