Directus中实现事件时间更新自动发邮件的Hook配置方案
Directus 事件时间更新通知Hook故障修复
预期实现逻辑
- 监听集合更新操作
- 判断触发更新的集合是否为
events集合 - 判断本次更新的字段是否包含
time_show_start、time_show_end等指定时间字段 - 从联系人集合中获取所有希望接收邮件通知的联系人
- 向上述联系人发送包含事件信息(含新旧时间数据)的通知邮件
参考效果截图:
故障根因
原代码无法正常运行由4个核心问题导致:
- 异步逻辑错误:
Array.forEach不会等待内部async函数执行,Hook触发后会立刻执行完返回,邮件发送、数据读取等异步操作会被运行时提前中断,根本无法执行完成 - 权限校验拦截:使用触发更新操作的普通用户上下文初始化服务,若该用户没有联系人集合读取、全量事件读取、邮件发送权限,会直接抛出权限错误终止流程
- 异常逻辑不合理:循环内单个联系人发信失败就直接抛出全局异常,中断整个发信流程,其余联系人无法收到通知;同时因为forEach不等待异步任务,抛出的异常无法被Hook框架正确捕获
- 内容格式错误:邮件
text字段传入了HTML标签,纯文本模式下标签会直接作为明文显示,不会被解析为加粗格式;且原逻辑未获取更新前的旧时间数据,无法满足新旧时间对比展示的需求
修正后可运行代码
module.exports = function registerHook({ action, filter }, { services, exceptions }) { const { MailService, ItemsService } = services; const { ServiceUnavailableException } = exceptions; // 用管理员权限初始化服务,绕开业务权限限制 const adminContext = { schema: null, accountability: { admin: true } }; // 暂存更新前的事件原始数据,key为事件ID,value为原始数据 const originalEventMap = new Map(); // 更新前钩子:提前读取原始事件数据,用于后续新旧值对比 filter('items.update', async (payload, { keys, collection, schema }) => { if (collection !== 'events') return payload; // 判断是否命中需要触发通知的时间字段 const targetFields = [ 'time_get_in', 'time_setup_start', 'time_setup_end', 'time_soundcheck_start', 'time_soundcheck_end', 'time_dinner', 'time_doors', 'time_show_start', 'time_show_end', 'time_curfew_live_music', 'time_curfew_guests' ]; const hasTargetField = targetFields.some(field => Object.keys(payload).includes(field)); if (!hasTargetField) return payload; adminContext.schema = schema; const eventsService = new ItemsService('events', adminContext); // 批量读取更新前的原始事件数据 const originalEvents = await eventsService.readMany(keys, { fields: ['*'] }); originalEvents.forEach(event => originalEventMap.set(event.id, event)); return payload; }); // 更新后钩子:执行发信逻辑 action('items.update', async ({ keys, collection, payload, schema }) => { if (collection !== 'events') return; const targetFields = [ 'time_get_in', 'time_setup_start', 'time_setup_end', 'time_soundcheck_start', 'time_soundcheck_end', 'time_dinner', 'time_doors', 'time_show_start', 'time_show_end', 'time_curfew_live_music', 'time_curfew_guests' ]; const hasTargetField = targetFields.some(field => Object.keys(payload).includes(field)); if (!hasTargetField) return; adminContext.schema = schema; const eventsService = new ItemsService('events', adminContext); const contactsService = new ItemsService('contacts', adminContext); const mailService = new MailService(adminContext); try { // 读取需要接收通知的联系人 const contacts = await contactsService.readByQuery({ filter: { getsmailon_eventtimechange: { _eq: true } }, fields: ['email'] }); // 读取更新后的事件数据 const updatedEvents = await eventsService.readMany(keys, { fields: ['*'] }); // 用for...of替代forEach,保证异步逻辑按顺序执行、可被await等待 for (const event of updatedEvents) { const originalEvent = originalEventMap.get(event.id) || {}; for (const contact of contacts) { try { await mailService.send({ to: contact.email, subject: `Zeit der Veranstaltung ${event.name} geändert`, text: `Zeit der Veranstaltung ${event.name} geändert`, html: `Zeit der Veranstaltung <b>${event.name}</b> geändert`, template: { name: 'event-time-update', data: { collection: collection, event: event, originalEvent: originalEvent, // 旧时间数据存在该字段中,模板可直接调用 changedFields: payload }, }, }); console.log('mail sent for event', event.id, 'to', contact.email); } catch (mailError) { // 单个联系人发信失败仅打印错误,不中断整体流程 console.error(`send mail to ${contact.email} failed for event ${event.id}:`, mailError); } } // 发信完成后清除暂存的原始数据,避免内存泄漏 originalEventMap.delete(event.id); } } catch (error) { console.error('event time update hook run failed:', error); throw new ServiceUnavailableException(error); } }); };
关键修改说明
- 替换所有异步
forEach循环为for...of,保证每一步异步操作都能被正确等待,不会被提前中断 - 新增更新前filter钩子,提前读取更新前的原始事件数据,传入邮件模板供新旧时间对比使用
- 统一使用管理员权限上下文初始化所有服务,避免普通用户权限不足导致的流程中断
- 调整异常处理逻辑:单个联系人发信失败仅打印错误日志,不终止整个发信流程;全局异常统一捕获抛出
- 补充邮件
html字段存放HTML格式内容,避免纯文本模式下标签明文显示 - 批量读取事件、联系人数据,替代循环内单条查询,减少数据库请求次数,提升执行效率
- 邮件模板数据中新增
originalEvent(旧事件数据)、changedFields(本次修改的字段及新值)字段,可直接在模板中渲染新旧时间对比内容
内容的提问来源于stack exchange,提问作者Lume
相关产品推荐
相关产品推荐

