feat(schedule): 待办提醒推送、行事历仅展示提醒与管理 API
单次提醒 worker 投递、local 时间写入校验、未来 7 天提醒列表与忽略/批量删除;行事历去掉事项重复展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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/remindLocal(YYYY-MM-DD HH:mm,用户时区),禁止自行估算 Unix 毫秒。`,
|
||||
);
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user