Compare commits

..

2 Commits

Author SHA1 Message Date
ddshi
e44183e3e0 chore: 提交剩余的 server 更改
- .env: 数据库路径配置
- scripts/add-reminder-times.cjs: 批量更新提醒时间脚本

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 11:11:14 +08:00
ddshi
15918f163c fix: 修复 reminder_times 处理和 UTC 时间存储
- server: 处理 reminder_times 更新时支持 null 值
- client: 统一使用 UTC 格式存储日期时间
- client: 修复 calculateNextDueDate 使用 UTC 时间计算
- client: 修复 calculateReminderTimes 使用 UTC 时间计算
- client: 修复 getReminderValueFromTimes 反推提醒选项
- client: 重写 createNextRecurringEventData 根据新日期重新计算 reminder_times
- 解决用户设置"14:00"显示"22:00"等时区问题
- 解决重复提醒自动创建时继承 reminder_times 问题

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 11:07:21 +08:00
6 changed files with 67 additions and 13 deletions

2
.env
View File

@ -8,7 +8,7 @@ JWT_EXPIRES_IN=7d
JWT_REFRESH_EXPIRES_IN=30d
# Database (SQLite for local development, PostgreSQL for production)
DATABASE_URL=E:/qia/server/prisma/dev.db
DATABASE_URL=file:./prisma/dev.db
# PostgreSQL (for production - Tencent Cloud)
# DATABASE_URL=postgresql://qia_admin:your-password@postgres.ap-shanghai.myqcloud.com:5432/qia

7
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "qia-server",
"version": "0.1.0",
"version": "0.2.0-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "qia-server",
"version": "0.1.0",
"version": "0.2.0-alpha",
"dependencies": {
"@libsql/client": "^0.17.0",
"@prisma/adapter-libsql": "^7.3.0",
@ -19,7 +19,6 @@
"express-validator": "^7.1.0",
"helmet": "^8.0.0",
"jsonwebtoken": "^9.0.2",
"sql.js": "^1.13.0",
"zod": "^3.23.8"
},
"devDependencies": {
@ -31,6 +30,7 @@
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.5.5",
"prisma": "^5.22.0",
"sql.js": "^1.13.0",
"tsx": "^4.19.0",
"typescript": "^5.6.2"
}
@ -2321,6 +2321,7 @@
"version": "1.13.0",
"resolved": "https://registry.npmmirror.com/sql.js/-/sql.js-1.13.0.tgz",
"integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==",
"dev": true,
"license": "MIT"
},
"node_modules/statuses": {

View File

@ -24,7 +24,6 @@
"express-validator": "^7.1.0",
"helmet": "^8.0.0",
"jsonwebtoken": "^9.0.2",
"sql.js": "^1.13.0",
"zod": "^3.23.8"
},
"devDependencies": {
@ -36,6 +35,7 @@
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.5.5",
"prisma": "^5.22.0",
"sql.js": "^1.13.0",
"tsx": "^4.19.0",
"typescript": "^5.6.2"
}

View File

@ -37,9 +37,12 @@ model Event {
date DateTime // For anniversaries: the date; For reminders: the reminder date
is_lunar Boolean @default(false)
repeat_type String @default("none") // 'yearly' | 'monthly' | 'none' (String for SQLite)
repeat_interval Int? // Weekly interval (only for weekly type)
is_holiday Boolean @default(false) // Only for anniversaries
is_completed Boolean @default(false) // Only for reminders
priority String @default("none") // 'none' | 'red' | 'green' | 'yellow' (String for SQLite)
next_reminder_date DateTime? // Next reminder date (calculated)
reminder_times String? // JSON array of reminder timestamps (ISO strings)
created_at DateTime @default(now())
updated_at DateTime @updatedAt

View File

@ -0,0 +1,26 @@
const initSqlJs = require('sql.js');
const fs = require('fs');
async function main() {
const SQL = await initSqlJs();
const db = new SQL.Database('e:/qia/server/prisma/dev.db');
try {
db.run("ALTER TABLE events ADD COLUMN reminder_times TEXT DEFAULT NULL");
console.log('reminder_times column added successfully');
} catch (error) {
if (error.message.includes('duplicate column name') || error.message.includes('already exists')) {
console.log('reminder_times column already exists');
} else {
console.error('Error:', error.message);
}
}
const data = db.export();
const buffer = Buffer.from(data);
fs.writeFileSync('e:/qia/server/prisma/dev.db', buffer);
db.close();
}
main().catch(console.error);

View File

@ -4,7 +4,7 @@ import db, { dbPath } from '../lib/db';
import { authenticateToken, AuthenticatedRequest } from '../middleware/auth';
import { asyncHandler } from '../middleware/errorHandler';
// Initialize database: Add priority column if it doesn't exist
// Initialize database: Add columns if they don't exist
const initDb = async () => {
try {
await db.execute({
@ -18,6 +18,19 @@ const initDb = async () => {
console.error('Error adding priority column:', error.message);
}
}
try {
await db.execute({
sql: `ALTER TABLE events ADD COLUMN reminder_times TEXT DEFAULT NULL`,
});
console.log('✓ Reminder times column added to events table');
} catch (error: any) {
if (error.message?.includes('duplicate column name') || error.message?.includes('already exists')) {
console.log('✓ Reminder times column already exists');
} else {
console.error('Error adding reminder_times column:', error.message);
}
}
};
// Run initialization
@ -31,7 +44,7 @@ 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().optional(), // Can be empty for reminders
date: z.string().optional(), // Can be empty string for reminders
is_lunar: z.boolean().default(false),
repeat_type: z.enum(['daily', 'weekly', 'monthly', 'yearly', 'none']).default('none'),
repeat_interval: z.number().int().positive().optional().nullable(), // 自定义间隔(周数)
@ -39,6 +52,7 @@ const createEventSchema = z.object({
is_holiday: z.boolean().default(false),
is_completed: z.boolean().default(false),
priority: z.enum(['none', 'red', 'green', 'yellow']).default('none'),
reminder_times: z.array(z.string()).optional(), // 提醒时间点数组JSON
});
const updateEventSchema = createEventSchema.partial();
@ -61,6 +75,7 @@ interface EventRow {
is_holiday: number;
is_completed: number;
priority: string; // 优先级none, red, green, yellow
reminder_times: string | null; // 提醒时间点数组JSON 字符串)
created_at: string;
updated_at: string;
}
@ -80,6 +95,7 @@ function formatEvent(event: EventRow) {
is_holiday: Boolean(event.is_holiday),
is_completed: Boolean(event.is_completed),
priority: event.priority as 'none' | 'red' | 'green' | 'yellow',
reminder_times: event.reminder_times ? JSON.parse(event.reminder_times) : undefined,
created_at: event.created_at,
updated_at: event.updated_at,
};
@ -139,11 +155,13 @@ router.post(
const repeatInterval = data.repeat_type === 'weekly' ? (data.repeat_interval || 1) : null;
// 如果前端传了 next_reminder_date 就使用它,否则根据 repeat_type 计算
const nextReminderDate = data.next_reminder_date || null;
// 存储 reminder_times 为 JSON 字符串
const reminderTimesValue = data.reminder_times ? JSON.stringify(data.reminder_times) : null;
await db.execute({
sql: `INSERT INTO events (id, user_id, type, title, content, date, is_lunar, repeat_type, repeat_interval, next_reminder_date, is_holiday, is_completed, priority, 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, data.priority],
sql: `INSERT INTO events (id, user_id, type, title, content, date, is_lunar, repeat_type, repeat_interval, next_reminder_date, is_holiday, is_completed, priority, reminder_times, 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, data.priority, reminderTimesValue],
});
const result = await db.execute({
@ -228,6 +246,12 @@ router.put(
args.push(data.priority);
}
// 处理 reminder_times 更新
if (data.reminder_times !== undefined) {
updates.push('reminder_times = ?');
args.push(data.reminder_times === null ? null : JSON.stringify(data.reminder_times));
}
if (updates.length > 0) {
updates.push('updated_at = datetime(\'now\')');
args.push(req.params.id);