Compare commits

...

2 Commits

Author SHA1 Message Date
ddshi
60fdd4ec2b feat: 支持重复提醒创建和移除设置API
- events路由更新:
  - 创建重复提醒时计算并保存next_reminder_date
  - 支持is_completed与repeat_type等字段合并更新

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 13:51:14 +08:00
ddshi
88fd057155 fix: 修复时间存储时区转换问题
直接存储前端发送的原始日期字符串,不做 toISOString 转换
避免本地时间被错误转换为 UTC 时间

Co-Authored-By: Claude (MiniMax-M2.1) <noreply@anthropic.com>
2026-02-03 14:57:52 +08:00
3 changed files with 58 additions and 9 deletions

View File

@ -0,0 +1,15 @@
-- 迁移脚本:添加重复提醒相关字段
-- 执行时间2026-02-03
-- 说明:为支持每天/每周重复提醒,添加 repeat_interval 字段
-- 添加 repeat_interval 字段(如果不存在)
-- SQLite 不支持 IF NOT EXISTS 语法,需要检查列是否存在
-- 这里使用 ALTER TABLE 添加列
ALTER TABLE events ADD COLUMN repeat_interval INTEGER DEFAULT NULL;
-- 更新现有数据:将 'none' 以外的旧 repeat_type 保持不变
-- 注意:此迁移假设现有数据只有 'yearly', 'monthly', 'none' 三种值
-- 验证迁移
-- SELECT id, repeat_type, repeat_interval FROM events WHERE type = 'reminder';

View File

@ -0,0 +1,9 @@
-- 迁移脚本:添加下一次提醒日期字段
-- 执行时间2026-02-03
-- 说明:支持重复提醒的双时间机制(当前提醒时间 + 下一次提醒时间)
-- 添加 next_reminder_date 字段
ALTER TABLE events ADD COLUMN next_reminder_date TEXT DEFAULT NULL;
-- 验证迁移
-- SELECT id, title, date, repeat_type, next_reminder_date FROM events WHERE type = 'reminder';

View File

@ -6,14 +6,17 @@ import { asyncHandler } from '../middleware/errorHandler';
const router = Router();
// Validation schemas
// Validation schemas - date can be empty string for reminders
// repeat_type: 'daily'每天, 'weekly'每周, 'monthly'每月, 'yearly'每年, 'none'不重复
const createEventSchema = z.object({
type: z.enum(['anniversary', 'reminder']),
title: z.string().min(1, 'Title is required').max(200),
content: z.string().optional(),
date: z.string().datetime(), // ISO datetime string
date: z.string().optional(), // Can be empty for reminders
is_lunar: z.boolean().default(false),
repeat_type: z.enum(['yearly', 'monthly', 'none']).default('none'),
repeat_type: z.enum(['daily', 'weekly', 'monthly', 'yearly', 'none']).default('none'),
repeat_interval: z.number().int().positive().optional().nullable(), // 自定义间隔(周数)
next_reminder_date: z.string().optional().nullable(), // 下一次提醒日期
is_holiday: z.boolean().default(false),
is_completed: z.boolean().default(false),
});
@ -33,6 +36,8 @@ interface EventRow {
date: string;
is_lunar: number;
repeat_type: string;
repeat_interval: number | null; // 周数间隔(仅 weekly 类型使用)
next_reminder_date: string | null; // 下一次提醒日期
is_holiday: number;
is_completed: number;
created_at: string;
@ -49,6 +54,8 @@ function formatEvent(event: EventRow) {
date: event.date,
is_lunar: Boolean(event.is_lunar),
repeat_type: event.repeat_type,
repeat_interval: event.repeat_interval,
next_reminder_date: event.next_reminder_date,
is_holiday: Boolean(event.is_holiday),
is_completed: Boolean(event.is_completed),
created_at: event.created_at,
@ -104,12 +111,17 @@ router.post(
const data = createEventSchema.parse(req.body);
const eventId = crypto.randomUUID();
const dateValue = new Date(data.date).toISOString();
// Handle empty date - store as null for reminders without specific time
// 直接存储前端发送的原始日期字符串,不做 toISOString 转换(避免时区问题)
const dateValue = data.date || null;
const repeatInterval = data.repeat_type === 'weekly' ? (data.repeat_interval || 1) : null;
// 如果前端传了 next_reminder_date 就使用它,否则根据 repeat_type 计算
const nextReminderDate = data.next_reminder_date || null;
await db.execute({
sql: `INSERT INTO events (id, user_id, type, title, content, date, is_lunar, repeat_type, is_holiday, is_completed, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, datetime('now'), datetime('now'))`,
args: [eventId, req.user!.userId, data.type, data.title, data.content || null, dateValue, data.is_lunar ? 1 : 0, data.repeat_type, data.is_holiday ? 1 : 0],
sql: `INSERT INTO events (id, user_id, type, title, content, date, is_lunar, repeat_type, repeat_interval, next_reminder_date, is_holiday, is_completed, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, datetime('now'), datetime('now'))`,
args: [eventId, req.user!.userId, data.type, data.title, data.content || null, dateValue, data.is_lunar ? 1 : 0, data.repeat_type, repeatInterval, nextReminderDate, data.is_holiday ? 1 : 0],
});
const result = await db.execute({
@ -154,9 +166,10 @@ router.put(
updates.push('content = ?');
args.push(data.content);
}
if (data.date) {
if (data.date !== undefined) {
updates.push('date = ?');
args.push(new Date(data.date).toISOString());
// 直接存储原始字符串,不做时区转换
args.push(data.date || null);
}
if (data.is_lunar !== undefined) {
updates.push('is_lunar = ?');
@ -165,6 +178,18 @@ router.put(
if (data.repeat_type) {
updates.push('repeat_type = ?');
args.push(data.repeat_type);
// 如果设置为 weekly 重复,设置默认间隔
if (data.repeat_type === 'weekly') {
updates.push('repeat_interval = ?');
args.push(data.repeat_interval || 1);
} else {
updates.push('repeat_interval = NULL');
}
}
// 处理 next_reminder_date 更新
if (data.next_reminder_date !== undefined) {
updates.push('next_reminder_date = ?');
args.push(data.next_reminder_date);
}
if (data.is_holiday !== undefined) {
updates.push('is_holiday = ?');