鸿蒙专家 发表于 2026-9-14 14:00:00

鸿蒙元服务ASCF日历日程:授权、时间戳类型与重复规则避坑

在鸿蒙元服务开发中,预约成功、会议提醒、生日或健身计划等场景,经常需要把日程自动写入系统日历。ASCF 提供了两个日历 API:addPhoneCalendar 用于添加单次日程,addPhoneRepeatCalendar 用于添加重复日程。它们能减少用户手动打开日历应用的操作,但实际接入时,授权、时间戳类型和重复规则很容易踩坑。下面按从权限到场景的顺序梳理。

一、权限要两步:声明 + 运行时授权

日历 API 的权限比较特殊,不是只在 module.json5 声明就能用。第一步是在 module.json5 中声明 ohos.permission.READ_CALENDAR 和 ohos.permission.WRITE_CALENDAR:


{
"requestPermissions": [
    {
      "name": "ohos.permission.READ_CALENDAR",
      "reason": "用于读取日历信息",
      "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
      }
    },
    {
      "name": "ohos.permission.WRITE_CALENDAR",
      "reason": "用于添加日历日程",
      "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
      }
    }
]
}


第二步是在调用 API 前申请 scope.addPhoneCalendar 运行时授权。原文作者一开始以为声明权限就够了,结果调用一直报错,后来才发现还需要这一步:


has.authorize({
scope: 'scope.addPhoneCalendar',
success: () => {
    console.info('授权成功');
    // 这里才能调用日历 API
},
fail: (err) => {
    console.error('授权失败:', err);
}
});


二、添加单次日程:addPhoneCalendar

授权成功后,可以用 addPhoneCalendar 添加单次日程。下面示例把当前时间往后推 1 小时作为开始时间,2 小时后作为结束时间,并提前 5 分钟提醒:


has.authorize({
scope: 'scope.addPhoneCalendar',
success: () => {
    const now = Math.floor(Date.now() / 1000);
    const startTime = now + 3600; // 1小时后
    const endTime = now + 7200; // 2小时后

    has.addPhoneCalendar({
      title: '项目评审会议',
      startTime: startTime,
      endTime: endTime,
      description: '讨论 Q4 项目进展',
      location: '会议室 A301',
      alarm: true,
      alarmOffset: 300, // 提前 5 分钟提醒
      success: () => {
      console.info('日程添加成功');
      has.showToast({ title: '已添加到日历' });
      },
      fail: (err) => {
      console.error('添加失败:', err);
      }
    });
}
});


这里最关键的是 startTime 和 endTime 都是 unix 时间戳,单位是秒,不是毫秒。JavaScript 的 Date.now() 返回毫秒,因此要除以 1000 并取整。

三、添加重复日程:addPhoneRepeatCalendar

重复日程使用 addPhoneRepeatCalendar。示例创建一个每周重复的周会,提前 10 分钟提醒,并设置 3 个月后结束重复:


has.authorize({
scope: 'scope.addPhoneCalendar',
success: () => {
    const now = Math.floor(Date.now() / 1000);
    const startTime = now + 3600;
    const endTime = now + 7200;

    // 重复结束时间:3个月后
    const repeatEndTime = now + 90 * 24 * 3600;

    has.addPhoneRepeatCalendar({
      title: '周会',
      startTime: startTime,
      endTime: endTime,
      description: '每周例会',
      location: '线上会议',
      alarm: true,
      alarmOffset: 600, // 提前 10 分钟
      repeatInterval: 'week',
      repeatEndTime: repeatEndTime,
      success: () => {
      console.info('重复日程添加成功');
      has.showToast({ title: '已添加到日历' });
      },
      fail: (err) => {
      console.error('添加失败:', err);
      }
    });
}
});


repeatInterval 默认是 month。如果不传 repeatEndTime,重复日程会一直重复下去。

四、常见业务场景

预约成功后添加日程:预约系统可以在用户预约成功后,把服务名、预约时间、地址、订单号写进日程,并提前 30 分钟提醒。


Page({
handleAppointmentSuccess(appointment) {
    has.authorize({
      scope: 'scope.addPhoneCalendar',
      success: () => {
      has.addPhoneCalendar({
          title: '预约: ' + appointment.serviceName,
          startTime: appointment.timestamp,
          endTime: appointment.timestamp + 3600,
          location: appointment.address,
          description: '预约编号: ' + appointment.orderNo,
          alarm: true,
          alarmOffset: 1800, // 提前 30 分钟
          success: () => {
            console.info('预约日程已添加');
          }
      });
      }
    });
}
});


生日提醒(每年重复):根据生日字符串构造今年的生日时间戳;如果今年生日已过,就用明年的。然后以 year 为重复周期添加,allDay 设为 true,提前 1 天提醒。


Page({
addBirthdayReminder(name, birthday) {
    // birthday 格式: '01-15' 表示 1月15日
    const parts = birthday.split('-');
    const now = new Date();
    const year = now.getFullYear();

    // 构造今年的生日时间戳
    const birthdayDate = new Date(year, parseInt(parts) - 1, parseInt(parts), 9, 0, 0);
    let startTime = Math.floor(birthdayDate.getTime() / 1000);

    // 如果今年的生日已过,用明年的
    if (startTime < Math.floor(Date.now() / 1000)) {
      birthdayDate.setFullYear(year + 1);
      startTime = Math.floor(birthdayDate.getTime() / 1000);
    }

    has.authorize({
      scope: 'scope.addPhoneCalendar',
      success: () => {
      has.addPhoneRepeatCalendar({
          title: name + ' 的生日',
          startTime: startTime,
          allDay: true,
          alarm: true,
          alarmOffset: 86400, // 提前1天
          repeatInterval: 'year',
          success: () => {
            has.showToast({ title: '生日提醒已添加' });
          }
      });
      }
    });
}
});


健身计划(每周重复):从下周周一早上 8 点开始,持续 1 小时,重复 3 个月。


Page({
addFitnessPlan() {
    // 下周一早上 8 点
    const now = new Date();
    const dayOfWeek = now.getDay();
    const daysUntilMonday = dayOfWeek === 0 ? 1 : 8 - dayOfWeek;
    const nextMonday = new Date(now.getFullYear(), now.getMonth(), now.getDate() + daysUntilMonday, 8, 0, 0);
    const startTime = Math.floor(nextMonday.getTime() / 1000);
    const endTime = startTime + 3600; // 1小时
    const repeatEndTime = startTime + 90 * 24 * 3600; // 3个月后

    has.authorize({
      scope: 'scope.addPhoneCalendar',
      success: () => {
      has.addPhoneRepeatCalendar({
          title: '健身时间',
          startTime: startTime,
          endTime: endTime,
          location: '健身房',
          description: '坚持就是胜利',
          alarm: true,
          alarmOffset: 1800,
          repeatInterval: 'week',
          repeatEndTime: repeatEndTime,
          success: () => {
            has.showToast({ title: '健身计划已添加' });
          }
      });
      }
    });
}
});


五、踩坑与排障

1. 时间戳单位是秒,不是毫秒。这是最常见的坑。Date.now() 和 new Date().getTime() 返回毫秒,但 API 要秒。需要:


const startTime = Math.floor(Date.now() / 1000);


如果直接用毫秒值,日程会被加到几十年后。

2. 时间参数类型不一致。原文作者前前后后踩了三次:第一次全用 number,报错 endTime is not type string;第二次全改成 String,又报错 startTime is not type number。后来才发现 endTime 要 string,startTime 和 repeatEndTime 要 number。


const now = Math.floor(Date.now() / 1000);
const startTime = now + 3600; // number
const endTime = String(now + 7200); // string,注意要转换
const repeatEndTime = now + 180 * 24 * 3600; // number


3. 每月重复不能超过 28 号。repeatInterval 为 month 时,日程日期不能大于 28 号。因为有些月份只有 28 天,系统无法确定“31 号”在这些月份该在哪天触发。如果要在月底重复,用 28 号更安全。

4. 授权必须在运行时完成。日历 API 不像有些 API 声明权限就行,必须通过 has.authorize 申请 scope.addPhoneCalendar。不授权直接调用会失败。

5. 不支持修改和删除。ASCF 的日历 API 只能添加,不能修改和删除已有日程。如果需要修改,得引导用户去系统日历应用手动操作。

6. allDay 事件仍要传 startTime。即使 allDay 为 true,startTime 还是需要 unix 时间戳;系统会根据这个时间戳的日期来确定是哪一天,时间部分会被忽略。

六、完整示例与 API 速查

原文还给出一个较完整的 Page 示例:data 中包含 title、description、location、repeatMode、repeatOptions、alarmOffset、lastResult;通过输入事件更新表单;addCalendar 先授权,再根据 repeatMode 是否为 none,决定调用 addPhoneCalendar 还是 addPhoneRepeatCalendar,重复结束时间设为半年后;addQuickMeeting 则快速添加一个 5 分钟后开始、1 小时 5 分钟后结束、提前 60 秒提醒的会议。核心逻辑与上文单次/重复示例一致。

对于大多数元服务场景,添加日程已经够用。如果确实需要查询或修改已有日程,ASCF 目前不支持,得使用系统的 @ohos.calendarManager 模块。总体来看,日历 API 适合预约、提醒、计划类场景,落地时重点处理授权和时间戳这两个容易出错的点。

热心网友5 发表于 2026-9-14 19:00:00

Re: 鸿蒙元服务ASCF日历日程:授权、时间戳类型与重复规则避坑

楼主这篇梳理得很清楚,尤其是权限要声明加运行时授权两步,这个太容易漏了,只改 module.json5 后调用失败确实很懵。单次日程里 startTime 和 endTime 用秒级时间戳也很关键,Date.now() 返回毫秒,忘了除以 1000 很容易写进去一个离谱的时间。重复日程那块,repeatInterval 默认是 month、不传 repeatEndTime 会一直重复,也提醒得很到位。预约成功、会议提醒这些场景确实用得上,等后续场景部分补完再跟着实践一下。

热心网友3 发表于 2026-9-14 19:10:00

Re: 鸿蒙元服务ASCF日历日程:授权、时间戳类型与重复规则避坑

楼主这篇很实用,把授权、时间戳和重复规则这几个容易出问题的地方都串起来了。两步权限这点特别关键,只在 module.json5 声明还不够,还得申请 scope.addPhoneCalendar,否则调用会一直失败。时间戳用秒而不是毫秒也是常见坑,Date.now() 一定要除以 1000 取整。单次日程和重复日程两个 API 的示例也清楚,尤其 repeatInterval 默认 month、不传 repeatEndTime 会一直重复,这两个细节很容易忽略。感谢整理,预约提醒这类场景正好能参考。

热心网友2 发表于 2026-9-14 19:20:00

Re: 鸿蒙元服务ASCF日历日程:授权、时间戳类型与重复规则避坑

感谢分享,这篇很实用。权限那部分确实是坑,只声明 READ_CALENDAR 和 WRITE_CALENDAR 还不够,还得运行时申请 scope.addPhoneCalendar,不然调用一直失败很让人摸不着头脑。时间戳必须用秒,Date.now() 要除以 1000 并取整,这点也很关键。重复日程里 repeatInterval 默认是 month、不传 repeatEndTime 会一直重复,这两个细节也提醒得好。后面做预约提醒时按这个思路来。
页: [1]
查看完整版本: 鸿蒙元服务ASCF日历日程:授权、时间戳类型与重复规则避坑