触发器设计
Closed this issue · 0 comments
sdjdd commented
触发器
触发器由条件和操作组成,在工单被创建、更新时触发,只有条件被满足时才会执行定义的操作。处于关闭状态的工单不会触发触发器,但工单被关闭时可触发触发器。
条件
条件分为 ALL 和 ANY ,定义在 ALL 中的条件必须全部被满足,定义在 ANY 中的条件只需满足一条即可。
Example:
{
"all": [
{"field": "updateType", "operator": "is", "value": "create"},
{"field": "status", "operator": "is", "value": 50}
],
"any": []
}
操作
[
{"field": "status", "value": 280},
{"field": "assigneeId", "value": "605303b48629e13ca9449f02"}
]
触发器执行逻辑
每当工单被创建、更新时,所有触发器会按顺序执行。如果一个触发器的条件被满足,将执行其操作并重新执行整个流程(跳过已执行的触发器),直到一次流程中没有触发器被触发。
触发器的操作会触发其他触发器,一个触发器在整个流程中只会被触发一次。
实现方案
将当前 Ticket 表 afterUpdate Hook 中的逻辑拆出来,以便在某一云引擎实例内手动执行
在 Ticket 表的 afterSave / afterUpdate hook 中:
- 获取 Trigger 表中的所有数据,并解析
- 按顺序执行所有触发器
- 手动执行 Ticket 表的 afterUpdate hook 中的逻辑,跳过触发器
/**
* @param {AV.Object} ticket
* @param {'create' | 'update'} updateType
*/
async function invokeTriggers(ticket, updateType) {
const status = ticket.get('status')
if (ticketStatus.isClosed(status) && !ticket.updatedKeys?.includes('status')) {
return
}
const ctx = new TriggerContext(ticket, updateType)
const triggers = await getActiveTriggers()
const triggerLogs = []
let finish = false
while (!finish) {
let count = 0
for (const trigger of triggers) {
if (trigger.triggered) {
continue
}
if (trigger.test(ctx)) {
trigger.exec(ctx)
trigger.triggered = true
count++
triggerLogs.push(
new AV.Object('TriggerLog', {
ticket: AV.Object.createWithoutData('Ticket', ticket.id),
trigger: AV.Object.createWithoutData('Trigger', trigger.object.id),
conditions: trigger.object.get('conditions'),
actions: trigger.object.get('actions'),
})
)
}
}
finish = count == 0
}
if (triggerLogs.length) {
AV.Object.saveAll(triggerLogs).catch(errorHandler.captureException)
}
if (!_.isEmpty(ctx.getDirtyAttrs())) {
const ticketToUpdate = AV.Object.createWithoutData('Ticket', ticket.id)
ticketToUpdate.disableAfterHook()
await ticketToUpdate.save(ctx.getDirtyAttrs(), { useMasterKey: true })
if (!ticket.updatedKeys) {
ticket.updatedKeys = []
}
Object.entries(ctx.getDirtyAttrs()).forEach(([key, value]) => {
ticket.set(key, value)
ticket.updatedKeys.push(key)
})
ticket.updatedKeys = _.uniq(ticket.updatedKeys)
afterUpdateTicketHandler(ticket, { skipTriggers: true })
}
}
/**
* @param {AV.Object} ticket
* @param {object} [options]
* @param {AV.User} [options.user]
* @param {boolean} [options.skipTriggers]
*/
async function afterUpdateTicketHandler(ticket, options) {
await ticket.fetch({ include: ['assignee'] }, { useMasterKey: true })
const user = options?.user || systemUser
const userInfo = await getTinyUserInfo(user)
if (ticket.updatedKeys?.includes('category')) {
addOpsLog(ticket, 'changeCategory', {
category: ticket.get('category'),
operator: userInfo,
})
}
if (ticket.updatedKeys?.includes('assignee')) {
const assigneeInfo = await getTinyUserInfo(ticket.get('assignee'))
addOpsLog(ticket, 'changeAssignee', {
assignee: assigneeInfo,
operator: userInfo,
})
notification.changeAssignee(ticket, user, ticket.get('assignee'))
}
if (ticket.updatedKeys?.includes('status')) {
if (ticketStatus.isClosed(ticket.get('status'))) {
AV.Cloud.run('statsTicket', { ticketId: ticket.id })
}
}
if (ticket.updatedKeys?.includes('evaluation') && options?.user) {
notification.ticketEvaluation(ticket, options.user, ticket.get('assignee'))
}
invokeWebhooks('ticket.update', {
ticket: ticket.toJSON(),
updatedKeys: ticket.updatedKeys,
})
if (!options?.skipTriggers) {
invokeTriggers(ticket, 'update', afterUpdateTicketHandler)
}
}