feat(schedule): 待办提醒推送、行事历仅展示提醒与管理 API

单次提醒 worker 投递、local 时间写入校验、未来 7 天提醒列表与忽略/批量删除;行事历去掉事项重复展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-30 02:02:58 +08:00
parent 7906f3c36d
commit 14f46cf318
20 changed files with 2453 additions and 452 deletions
+61
View File
@@ -150,3 +150,64 @@ export function formatLocalTime(epochMs, timezone = DEFAULT_TIMEZONE) {
const parts = getLocalParts(epochMs, timezone);
return `${pad2(parts.hour)}:${pad2(parts.minute)}`;
}
/**
* Parse a wall-clock datetime in the given timezone, e.g. "2026-07-01 06:00".
*/
export function parseLocalDateTimeString(value, timezone = DEFAULT_TIMEZONE) {
const raw = String(value ?? '').trim();
if (!raw) return null;
const match = raw.match(
/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/,
);
if (!match) {
throw new Error(`无法解析本地时间「${raw}」,请使用 YYYY-MM-DD HH:mm`);
}
return zonedTimeToEpochMs(
{
year: Number(match[1]),
month: Number(match[2]),
day: Number(match[3]),
hour: Number(match[4] ?? 0),
minute: Number(match[5] ?? 0),
second: Number(match[6] ?? 0),
},
timezone,
);
}
export function resolveScheduleTimestamp({
epochMs = null,
localString = null,
timezone = DEFAULT_TIMEZONE,
fieldName = '时间',
now = Date.now(),
} = {}) {
const local = String(localString ?? '').trim();
if (local) return parseLocalDateTimeString(local, timezone);
if (epochMs == null || epochMs === '') return null;
return assertReasonableScheduleEpoch(epochMs, { now, fieldName });
}
export function assertReasonableScheduleEpoch(
epochMs,
{ now = Date.now(), fieldName = '时间', maxPastDays = 2, maxFutureDays = 400 } = {},
) {
const safe = Number(epochMs);
if (!Number.isFinite(safe) || safe <= 0) {
throw new Error(`${fieldName}无效,请改用 YYYY-MM-DD HH:mm 的 local 字段`);
}
const min = now - maxPastDays * 86400000;
const max = now + maxFutureDays * 86400000;
if (safe < min || safe > max) {
const label = new Intl.DateTimeFormat('zh-CN', {
timeZone: normalizeTimezone(process.env.H5_DEFAULT_TIMEZONE),
dateStyle: 'short',
timeStyle: 'short',
}).format(new Date(safe));
throw new Error(
`${fieldName}${label})超出合理范围。请改用 startLocal/remindLocalYYYY-MM-DD HH:mm,用户时区),禁止自行估算 Unix 毫秒。`,
);
}
return safe;
}