diff --git a/api/admin/v1/notification.go b/api/admin/v1/notification.go index 134645a..775e09c 100644 --- a/api/admin/v1/notification.go +++ b/api/admin/v1/notification.go @@ -107,7 +107,7 @@ type MessageSendRes struct{} type MessageBroadcastReq struct { g.Meta `path:"/notification/messages/broadcast" method:"post" mime:"json" tags:"管理后台-通知" summary:"广播消息"` - TenantID int64 `json:"tenant_id" v:"required|min:1"` + TenantID *int64 `json:"tenant_id" dc:"目标租户ID,不传或为空表示广播到所有租户"` Title string `json:"title" v:"required"` Content string `json:"content" v:"required"` TargetRoles string `json:"target_roles" d:"" dc:"目标角色,逗号分隔如 owner,admin;留空表示全部角色"` diff --git a/api/admin/v1/tenant_level.go b/api/admin/v1/tenant_level.go index 00e8fd1..63255c3 100644 --- a/api/admin/v1/tenant_level.go +++ b/api/admin/v1/tenant_level.go @@ -44,9 +44,14 @@ type TenantLevelConfigCreateRes struct { } 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"` + g.Meta `path:"/tenant-level-configs/{id}" method:"put" mime:"json" tags:"管理后台-租户等级" summary:"更新等级配置"` + Id int64 `json:"id" in:"path" v:"required|min:1"` + Name *string `json:"name" dc:"等级名称"` + CumulativeRechargeThreshold *float64 `json:"cumulative_recharge_threshold" dc:"累计充值阈值"` + MaxMembers *int `json:"max_members" dc:"最大成员数"` + MaxConcurrency *int `json:"max_concurrency" dc:"最大并发数"` + PriceMultiplier *float64 `json:"price_multiplier" dc:"价格乘数"` + SortOrder *int `json:"sort_order" dc:"排序权重"` } type TenantLevelConfigUpdateRes struct{} diff --git a/go.mod b/go.mod index bf60cca..4cba432 100644 --- a/go.mod +++ b/go.mod @@ -72,7 +72,7 @@ require ( github.com/olekukonko/tablewriter v1.1.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/pressly/goose/v3 v3.27.1 // indirect - github.com/redis/go-redis/v9 v9.12.1 // indirect + github.com/redis/go-redis/v9 v9.19.0 // indirect github.com/richardlehane/mscfb v1.0.6 // indirect github.com/richardlehane/msoleps v1.0.6 // indirect github.com/rivo/uniseg v0.2.0 // indirect @@ -92,6 +92,7 @@ require ( go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/image v0.25.0 // indirect golang.org/x/net v0.53.0 // indirect diff --git a/go.sum b/go.sum index eed80ef..a69a12e 100644 --- a/go.sum +++ b/go.sum @@ -134,6 +134,8 @@ github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5s github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg= github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k= +github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8= github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= @@ -211,6 +213,8 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/internal/logic/admin/model.go b/internal/logic/admin/model.go index 9c0cf4a..c701481 100644 --- a/internal/logic/admin/model.go +++ b/internal/logic/admin/model.go @@ -152,9 +152,13 @@ func (s *sAdmin) CreateModel(ctx context.Context, req *v1.ModelCreateReq) (*v1.M "max_context_tokens": req.MaxContext, "max_output_tokens": req.MaxOutput, "description": req.Description, - "tags": req.Tags, "status": "active", } + if req.Tags != nil { + insertData["tags"] = req.Tags + } else { + insertData["tags"] = []string{} + } if req.Capabilities != nil { capJson, _ := json.Marshal(req.Capabilities) insertData["capabilities"] = string(capJson) diff --git a/internal/logic/admin/notification.go b/internal/logic/admin/notification.go index e81d42d..bb56424 100644 --- a/internal/logic/admin/notification.go +++ b/internal/logic/admin/notification.go @@ -7,6 +7,7 @@ import ( "text/template" "time" + "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/admin/v1" @@ -39,17 +40,16 @@ func (s *sAdmin) ListTemplates(ctx context.Context, req *v1.TemplateListReq) (*v // GetTemplate 获取单个通知模板 func (s *sAdmin) GetTemplate(ctx context.Context, req *v1.TemplateGetReq) (*v1.TemplateGetRes, error) { - var tpl map[string]any - err := dao.NtfTemplates.Ctx(ctx). + record, err := dao.NtfTemplates.Ctx(ctx). Where("code", req.Code). - Scan(&tpl) + One() if err != nil { return nil, err } - if tpl == nil { + if record == nil { return nil, common.NewNotFoundError("template") } - return &v1.TemplateGetRes{Data: tpl}, nil + return &v1.TemplateGetRes{Data: record.Map()}, nil } // UpdateTemplate 更新通知模板 @@ -150,7 +150,25 @@ func (s *sAdmin) SendBroadcast(ctx context.Context, req *v1.MessageBroadcastReq) } engine := common.NewNotificationEngine() - return nil, engine.SendBroadcastMessage(ctx, req.TenantID, "system", req.Title, req.Content, req.TargetRoles) + + if req.TenantID != nil && *req.TenantID > 0 { + return nil, engine.SendBroadcastMessage(ctx, *req.TenantID, "system", req.Title, req.Content, req.TargetRoles) + } + + // 广播到所有活跃租户 + var tenants []struct { + Id int64 `json:"id"` + } + err := dao.TntTenants.Ctx(ctx).Where("status", "active").Fields("id").Scan(&tenants) + if err != nil { + return nil, err + } + for _, t := range tenants { + if e := engine.SendBroadcastMessage(ctx, t.Id, "system", req.Title, req.Content, req.TargetRoles); e != nil { + g.Log().Warningf(ctx, "SendBroadcast to tenant %d failed: %v", t.Id, e) + } + } + return nil, nil } // ListMessages 获取所有消息列表(管理后台,支持过滤) diff --git a/internal/logic/admin/plan.go b/internal/logic/admin/plan.go index 1864b32..6973cc5 100644 --- a/internal/logic/admin/plan.go +++ b/internal/logic/admin/plan.go @@ -112,15 +112,14 @@ func (s *sAdmin) ArchivePlan(ctx context.Context, req *v1.PlanArchiveReq) (*v1.P // ToggleRecommend 切换推荐标记 func (s *sAdmin) ToggleRecommend(ctx context.Context, req *v1.PlanToggleRecommendReq) (*v1.PlanToggleRecommendRes, error) { - // 先查当前值 - var isRecommended bool - err := dao.PlnPlans.Ctx(ctx). + val, err := dao.PlnPlans.Ctx(ctx). Where("id", req.Id). Fields("is_recommended"). - Scan(&isRecommended) + Value() if err != nil { return nil, err } + isRecommended := val.Bool() _, err = dao.PlnPlans.Ctx(ctx). Where("id", req.Id). diff --git a/internal/logic/admin/tenant.go b/internal/logic/admin/tenant.go index 1baba5b..e70e29b 100644 --- a/internal/logic/admin/tenant.go +++ b/internal/logic/admin/tenant.go @@ -2,10 +2,12 @@ package admin import ( "context" + "encoding/json" "strings" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/admin/v1" @@ -431,9 +433,18 @@ func (s *sAdmin) UpdateTenant(ctx context.Context, req *v1.TenantUpdateReq) (*v1 // UpdateTenantChannelScope 更新租户默认渠道范围 func (s *sAdmin) UpdateTenantChannelScope(ctx context.Context, req *v1.TenantChannelScopeUpdateReq) (*v1.TenantChannelScopeUpdateRes, error) { - _, err := dao.TntTenants.Ctx(ctx).Where("id", req.Id).Update(do.TntTenants{ - DefaultChannelScope: req.DefaultChannelScope, - }) + var scopeValue any + if req.DefaultChannelScope == nil || *req.DefaultChannelScope == "" || *req.DefaultChannelScope == "all" { + scopeValue = nil + } else if json.Valid([]byte(*req.DefaultChannelScope)) { + scopeValue = *req.DefaultChannelScope + } else { + return nil, gerror.New("default_channel_scope 必须是有效的 JSON(如 [1,5,12])或 null") + } + + _, err := dao.TntTenants.Ctx(ctx).Where("id", req.Id).Data(g.Map{ + "default_channel_scope": scopeValue, + }).Update() if err != nil { return nil, err } diff --git a/internal/logic/admin/tenant_level.go b/internal/logic/admin/tenant_level.go index 13b5cd6..c8a041c 100644 --- a/internal/logic/admin/tenant_level.go +++ b/internal/logic/admin/tenant_level.go @@ -4,6 +4,7 @@ import ( "context" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" "github.com/qianfree/team-api/api/admin/v1" "github.com/qianfree/team-api/internal/dao" @@ -64,9 +65,33 @@ func (s *sAdmin) CreateTenantLevelConfig(ctx context.Context, req *v1.TenantLeve } func (s *sAdmin) UpdateTenantLevelConfig(ctx context.Context, req *v1.TenantLevelConfigUpdateReq) (*v1.TenantLevelConfigUpdateRes, error) { + data := g.Map{} + if req.Name != nil { + data["name"] = *req.Name + } + if req.CumulativeRechargeThreshold != nil { + data["cumulative_recharge_threshold"] = *req.CumulativeRechargeThreshold + } + if req.MaxMembers != nil { + data["max_members"] = *req.MaxMembers + } + if req.MaxConcurrency != nil { + data["max_concurrency"] = *req.MaxConcurrency + } + if req.PriceMultiplier != nil { + data["price_multiplier"] = *req.PriceMultiplier + } + if req.SortOrder != nil { + data["sort_order"] = *req.SortOrder + } + + if len(data) == 0 { + return &v1.TenantLevelConfigUpdateRes{}, nil + } + _, err := dao.TntTenantLevelConfigs.Ctx(ctx). Where("id", req.Id). - Data(req.Update). + Data(data). Update() if err != nil { return nil, err @@ -75,20 +100,16 @@ func (s *sAdmin) UpdateTenantLevelConfig(ctx context.Context, req *v1.TenantLeve } 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 - } + dao.TntTenantLevelConfigs.Ctx(ctx).Where("id", req.Id).Scan(&config) if config.Id == 0 { - return nil, gerror.New("等级配置不存在") + return &v1.TenantLevelConfigDeleteRes{}, nil } if config.Level == 1 { return nil, gerror.New("不允许删除默认等级 LV1") } - _, err = dao.TntTenantLevelConfigs.Ctx(ctx).Where("id", req.Id).Delete() + _, err := dao.TntTenantLevelConfigs.Ctx(ctx).Where("id", req.Id).Delete() if err != nil { return nil, err } diff --git a/internal/logic/admin/tenant_model.go b/internal/logic/admin/tenant_model.go index c98f6d5..d8422c5 100644 --- a/internal/logic/admin/tenant_model.go +++ b/internal/logic/admin/tenant_model.go @@ -137,7 +137,7 @@ func (s *sAdmin) BatchAssignModels(ctx context.Context, req *v1.TenantModelBatch CustomPricingTiers: tiersJSON, Multiplier: 1.0, } - g.Log().Debugf(ctx, "BatchAssignModels: inserting data tenant_id=%v, model_id=%v, enabled=%v, max_concurrency=%v", insertData.TenantId, insertData.ModelId, insertData.Enabled, insertData.MaxConcurrency) + g.Log().Debugf(ctx, "BatchAssignModels: inserting data tenant_id=%v, model_id=%v, enabled=%v, max_concurrency=%v", insertData.TenantId, insertData.ModelId, insertData.Enabled, *maxConc) _, err = dao.MdlTenantModels.Ctx(ctx).Insert(insertData) if err != nil { g.Log().Errorf(ctx, "BatchAssignModels: insert failed, model_id=%d, err=%v", a.ModelID, err) diff --git a/internal/logic/monitor/alert_notify.go b/internal/logic/monitor/alert_notify.go index 41fd410..32fa172 100644 --- a/internal/logic/monitor/alert_notify.go +++ b/internal/logic/monitor/alert_notify.go @@ -116,7 +116,7 @@ func sendAlertInAppToAdmins(ctx context.Context, adminIDs []int64, title, conten Type: "alert", Title: title, Content: content, - IsRead: false, + IsRead: 0, }) if err != nil { g.Log().Warningf(ctx, "create in-app alert for admin %d: %v", adminID, err) diff --git a/internal/logic/monitor/alert_rule.go b/internal/logic/monitor/alert_rule.go index bbcf3f2..fc5fbd5 100644 --- a/internal/logic/monitor/alert_rule.go +++ b/internal/logic/monitor/alert_rule.go @@ -81,6 +81,11 @@ func CreateAlertRule(ctx context.Context, data do.OpsAlertRules) (int64, error) return 0, common.NewBadRequestError("规则名称不能为空") } metricType := gconv.String(data.MetricType) + // Auto-prefix: if no namespace (dot) found, try prepending "api." + if !strings.Contains(metricType, ".") { + metricType = "api." + metricType + data.MetricType = metricType + } if _, ok := validMetricTypes[metricType]; !ok { return 0, gerror.Newf("不支持的指标类型: %s", metricType) } @@ -116,6 +121,10 @@ func UpdateAlertRule(ctx context.Context, id int64, updates do.OpsAlertRules) er // Validate metric_type if provided if updates.MetricType != nil { metricType := gconv.String(updates.MetricType) + if !strings.Contains(metricType, ".") { + metricType = "api." + metricType + updates.MetricType = metricType + } if _, valid := validMetricTypes[metricType]; !valid { return gerror.Newf("不支持的指标类型: %s", metricType) }