限制订阅预警数量
1. 概述
为了保障系统稳定运行,避免任务并发数量过多引起性能问题,支持管理员对订阅/预警的启用数量进行精细化限制,从以下两个维度进行管控:
- 按系统、用户/用户组限制总量:支持设置系统中总共启用的订阅/预警上限,支持按单个用户/用户组设置允许启用的订阅/预警上限
- 按发送时间段限制总量:若多个订阅/预警的发送时间在特定时间段(如每天早上8点-9点)内,限制用户可启用的订阅和预警数量。
达到上限后时,您将无法继续启用订阅/预警。
- 本限制仅针对订阅/预警的启用操作(包括创建并启用、编辑后保存启用、直接启用),不影响历史订阅/预警的发送。
- 限制规则仅对在BI产品界面上的手动操作生效,通过API接口进行的启用/停用操作不受此限制。
2. 使用指导
2.1. 操作步骤
- 创建对应的订阅预警数量管理插件。
- 按照实际的管理诉求在对应配置项中分别填入订阅、预警的限制。
- 启用插件,限制即刻生效。
2.2. 按系统、用户/用户组分别限制订阅、预警的启用数量
2.2.1. 功能介绍
以订阅为例:
管理员启用该前端插件后,对插件内的配置项进行编辑,参考图中框选部分。

- 系统级别限制:未设置用户/用户组的启用限制时,系统默认的限制数量。
- 按用户级别限制:根据用户限制数量,格式为
{用户ID:数量}。 - 按用户组级别限制:根据用户组限制数量,格式为
{用户组ID:数量}。 - 优先级:用户设置 > 按用户组设置 > 系统设置;如果您有专属的个人上限,则以该数量为准;否则,将依次继承您所在用户组、系统的设置。
在您进行以下操作时,系统会校验总量(按T-1的数量对比)是否已达上限:
- 创建新的订阅,将状态改为启用,并点击【保存】时;
- 编辑一个订阅,将状态改为启用,并点击【保存】时;
- 在列表中对一个已停用的订阅直接点击【启用】按钮时。
如果您的启用数量已达到上限,则无法再次成功操作启用。
2.2.2. 配置说明
本功能通过插件进行配置,通常由系统管理员操作。在插件代码中开启「订阅预警启用数量限制」并配置「订阅预警的总限制数」,参考图中框选部分。
如何创建插件以及插件代码完整示例可参考插件管理。

2.3. 按发送时间段分别限制订阅、预警的总量
2.3.1. 功能介绍
除了总量限制,还支持限制在特定时间段内(例如一天中的某个小时)新启用的订阅和预警数量。
按「发送时间段限制」优先级高于「按启用数量限制」。
-
配置方式:
可按小时为单位,设置每天、每周或每月内允许启用的数量上限。 -
统计与校验逻辑:
- 系统校验的是截止到今日00:00的历史存量数据。即,统计的是在今天之前已经在该时段内启用的数量,而非实时查询当前用户今日新启用的数量。
- 在【保存】和【启用】操作时,系统会校验在当前时间所属的时段内,您历史已启用的数量是否已达上限。
-
限制提示:
如果在该时段内的历史启用数量已达上限,操作将无法成功,并会看到提示:“启用的订阅(预警)数量在该时段内已达上限”。
2.3.2. 配置说明
本功能通过插件进行配置,通常由系统管理员操作,在插件代码中开启「订阅预警启用数量限制」并配置「订阅预警按小时段限制」的具体数量,参考图中框选部分。
如何创建插件以及插件代码完整示例可参考插件管理。

2.4. 插件管理
2.4.1. 配置插件
入口:管理中心 > 开放平台 > 插件管理
- 点击右上角「新建插件」,配置完成插件基础信息后点击「确定」,完成插件新建。
- 点击上一步新建完成插件右侧的「编辑代码」,在弹出的代码编辑器中,输入插件代码后点击「确定」,插件代码可参考插件代码示例。
- 打开启用开关,启用该插件。

插件管理更多操作可参考插件管理。
2.4.2. 代码示例
以下代码示例展示了如何配置订阅和预警的限制规则。您可以根据实际需要修改其中的参数。
// 订阅限制配置
const scheduleLimitedConfig = {
enabled: true, // 订阅启用数量限制
// 【订阅启用的总数限制】:支持「系统默认、按用户/用户组」设置,优先级:用户 > 用户组 > 默认
userLimitedMap: {}, // 按用户限制数量 {uIds: number} 例如{'uId1,uId2,uId3': 10, 'uId4,uId5,uId6': 20}
groupLimitedMap: {}, // 按用户组限制数量 {groupIds: number} 例如{'groupId1,groupId2,groupId3': 10, 'groupId4,groupId5,groupId6': 20}
defaultLimitedCount: 5, // 未设置用户/用户组的启用限制时,系统默认的限制数量
// 【订阅按小时段限制】 优先级高于 【订阅启用的总数限制】
hourLimitedMap: {
'04': 0,
'06': 2,
'08': 5,
}, // 按定时执行的小时数限制数量{hour: number} 例如{'00': 10, '01': 20}代表0-1点限制10个,1-2点限制20个
}
// 预警限制配置
const alertLimitedConfig = {
enabled: true, // 预警启用数量限制
// 【预警启用的总数限制】,支持「系统默认、按用户/用户组」设置,优先级:用户 > 用户组 > 默认
userLimitedMap: {}, // 按用户限制数量 {uIds: number} 例如{'uId1,uId2,uId3': 10, 'uId4,uId5,uId6': 20}
groupLimitedMap: {}, // 按用户组限制数量 {groupIds: number} 例如{'groupId1,groupId2,groupId3': 10, 'groupId4,groupId5,groupId6': 20}
defaultLimitedCount: 10, // 默认限制数量
// 【预警按小时段限制】 优先级高于【预警启用的总数限制】
hourLimitedMap: {
'00': 1,
'04': 2,
'06': 2,
'08': 5,
}, // 按定时执行的小时数限制数量{hour: number} 例如{'00': 10, '01': 20}标识0-1点限制10个,1-2点限制20个
}
//后续代码用于逻辑校验,管理员通常无需修改
const prefix = '' // 域名前缀,没有则留空
class LimitedBase {
constructor(limitedMap, type) {
this.limitedMap = limitedMap
this.type = type
this.hourCreatedCount = {}
this.userCreatedCount = 0
this.cacheInfo = {}
this.hoursDataFetched = false
this.countType = type === 'schedule' ? 'subscription' : type
}
// 解析定时发送配置
parseCronHours = (cronValue) => {
if (cronValue) {
const cronInfo = JSON.parse(cronValue)
return (cronInfo.hours || '').split(',')
}
return []
}
// 获取用户组
getUserGroup = async () => {
if (Object.keys(this.limitedMap.groupLimitedMap).length === 0) return []
const response = await window.GD.fetch(`/api/user/groups`)
const { response: resJson } = await response.json()
return resJson.map(group => group.ugId)
}
// 获取各时间段创建数量
getCronCreateCount = async () => {
const response = await window.GD.fetch(`/api/${this.type}/statistics-cron`)
const { response: resJson } = await response.json()
this.hourCreatedCount = resJson
}
// 获取用户创建的启用数量
getUserCreateCount = async () => {
const response = await window.GD.fetch(`/api/resource-control/count/${this.countType}?enabledStatus=1`)
const { response: resJson } = await response.json()
this.userCreatedCount = resJson
}
// 判断时间段维度是否达到限制
hourLimitHasReached = async (hours) => {
if (hours?.length) {
if (!this.hoursDataFetched) {
await this.getCronCreateCount()
this.hoursDataFetched = true
}
return hours.some(hour => {
const existedCount = this.hourCreatedCount[ hour ] || 0
const limitedCount = this.limitedMap.hourLimitedMap[ hour ]
return limitedCount && existedCount >= limitedCount
})
}
return false
}
// 判断用户维度是否受到限制
userLimitHasReached = async () => {
const userInfo = GD.getUser()
const { userLimitedMap, groupLimitedMap, defaultLimitedCount } = this.limitedMap
await this.getUserCreateCount()
// 用户级别判断
const userLimitedConfigKey = Object.keys(userLimitedMap).find(ids => ids.includes(userInfo.uId))
if (userLimitedConfigKey) {
return {
limited: this.userCreatedCount >= userLimitedMap[ userLimitedConfigKey ],
type: '用户',
}
}
// 用户组级别判断
const groupIds = await this.getUserGroup()
const groupLimitedConfigKey = Object.keys(groupLimitedMap)
const groupLimitedList = groupIds.map(groupId => groupLimitedConfigKey.find(ids => ids.includes(groupId))).filter(Boolean)
if (groupLimitedList.length) {
return {
limited: groupLimitedList.every(limitedConfigKey => {
const limitedCount = groupLimitedMap[ limitedConfigKey ]
return this.userCreatedCount >= limitedCount
}),
type: '用户组'
}
}
// 默认判断
return {
limited: this.userCreatedCount >= defaultLimitedCount,
type: '系统',
}
}
limitHasReachedHandler = async (hours) => {
const ret = await this.hourLimitHasReached(hours)
if (ret) {
return {
limited: true,
type: '触发时段',
}
}
return await this.userLimitHasReached()
}
// 判断是否需要校验,以及需要校验的时间段
handleIfLimited = async (editedInfo, id) => {
const { cron, enabled } = editedInfo
if (!enabled) {
return {
limited: false
}
}
const hours = this.parseCronHours(cron?.value)
const cacheData = this.cacheInfo[ id ]
if (!cacheData) return await this.limitHasReachedHandler(hours)
const { cronValue: cacheCronValue, enabled: cacheEnabled, creatorId } = cacheData
if (cacheEnabled !== enabled) {
const userInfo = GD.getUser()
// 开启订阅,如果创建者非当前用户,则判断是否达到触发时段上限
if (creatorId === userInfo.uId) {
return await this.limitHasReachedHandler(hours)
}
return {
limited: await this.hourLimitHasReached(hours),
type: '触发时段',
}
}
const cacheHours = this.parseCronHours(cacheCronValue)
const diffHours = hours.filter(hour => !cacheHours.includes(hour))
return {
limited: await this.hourLimitHasReached(diffHours),
type: '触发时段',
}
}
}
class ScheduleLimited extends LimitedBase {
constructor(limitedMap, type) {
super(limitedMap, type)
this.initListener()
}
// 更新缓存订阅数据
updateCacheInfo = ({ schedules }) => {
schedules.forEach((item) => {
this.cacheInfo[ item.scId ] = {
cronValue: item.cron?.value,
enabled: item.enabled,
creatorId: item.creator?.relId,
}
})
}
// 接口监听
initListener = () => {
const getScheduleUrlReg = new RegExp(`^${prefix}/api/schedule/[A-Z_]+$`)
const editScheduleUrlReg = new RegExp(`^${prefix}/api/schedule/update/[a-zA-Z0-9]+$`)
const addScheduleUrl = `${prefix}/api/schedule/add`
window.GD.registerFetchMiddleware(
'订阅数量限制相关接口处理',
async ({ url, body, method }) => {
if (
method === 'POST'
&& (editScheduleUrlReg.test(url) || url === addScheduleUrl)
) {
const { limited, type } = await this.handleIfLimited(body, body.scId)
if (limited) {
console.log('达到上限', type)
return {
errorMessage: type === '触发时段' ? '启用的订阅数量在该时段内已达上限' : '您启用的订阅数量已达上限'
}
}
}
},
async ({ url, method, response }) => {
if (method === 'POST' && getScheduleUrlReg.test(url)) {
this.updateCacheInfo(response)
} else if (
method === 'POST'
&& (editScheduleUrlReg.test(url) || url === addScheduleUrl)
) {
const { scId, enabled } = response
if (this.cacheInfo[ scId ]) {
this.cacheInfo[ scId ].enabled = enabled
}
}
},
200,
)
}
}
class AlertLimited extends LimitedBase {
constructor(limitedMap, type) {
super(limitedMap, type)
this.initListener()
}
// 更新缓存预警数据
updateCacheInfo = ({ alerts }) => {
alerts.forEach((item) => {
this.cacheInfo[ item.alertId ] = {
cronValue: item.cron?.value,
enabled: item.enabled,
creatorId: item.creator?.uId,
}
})
}
initListener = () => {
const getAlertUrl = `${prefix}/api/alert/search`
const batchEditAlertUrl = `${prefix}/api/alert`
const editAlertUrlReg = new RegExp(`^${prefix}/api/oriput/alert/[a-zA-Z0-9]+$`)
window.GD.registerFetchMiddleware(
'预警数量限制相关接口处理',
async ({ url, body, method }) => {
if (
method === 'POST'
&& editAlertUrlReg.test(url)
) {
const { limited, type } = await this.handleIfLimited(body, body.alertId)
if (limited) {
console.log('达到上限', type)
return {
errorMessage: type === '触发时段' ? '启用的预警数量在该时段内已达上限' : '您启用的预警数量已达上限'
}
}
}
if (
method === 'POST'
&& url === batchEditAlertUrl
) {
let limited = false
let type = ''
for (const alert of body) {
const { limited: _limited, type: _type } = await this.handleIfLimited(alert, alert.alertId)
if (_limited) {
limited = _limited
type = _type
break
}
}
if (limited) {
console.log('达到上限', type)
return {
errorMessage: type === '触发时段' ? '启用的预警数量在该时段内已达上限' : '您启用的预警数量已达上限'
}
}
}
},
async ({ url, method, response }) => {
if (method === 'POST' && url === getAlertUrl) {
this.updateCacheInfo(response)
} else if (
method === 'POST'
&& editAlertUrlReg.test(url)
) {
const { alertId, enabled, cron } = response
if (this.cacheInfo[ alertId ]) {
this.cacheInfo[ alertId ].enabled = enabled
this.cacheInfo[ alertId ].cronValue = cron?.value
}
}
},
200,
)
}
}
window.GD.on('gd-ready', () => {
if (scheduleLimitedConfig.enabled) {
new ScheduleLimited(scheduleLimitedConfig, 'schedule')
}
if (alertLimitedConfig.enabled) {
new AlertLimited(alertLimitedConfig, 'alert')
}
})
3. 常见问题 (FAQ)
Q1: 如果我已经启用的订阅数量超过了新设置的上限,会怎么样?
A1: 没有任何影响。限制规则只针对新增的启用操作,不会改变或停用您已经处于启用状态的订阅或预警。
Q2: 是否可以临时关闭这些限制?
A2: 可以。管理员可以在插件代码中,将 scheduleLimitedConfig 或 alertLimitedConfig 中的 enabled 属性设置为 false,并重新启用插件即可临时关闭相应限制。
Q3: 为什么我在某个小时段内一个都没启用,却提示我“已达上限”?
A3: 频率限制校验的是历史存量数据。提示出现说明在今天的这个小时段之前,您名下已经启用了足够多在该时段触发的订阅/预警,触发了上限。您需要联系管理员调整该时段的限额,或停用一些旧的订阅/预警。
Q4: 这些限制对通过API接口操作生效吗?
A4: 不生效。此插件限制仅对在BI产品界面上进行的【保存】和【启用】操作生效,通过API接口启用或停用不受限制。