You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 05:24:29