# 纪念日管理系统 - 技术架构文档 ## 1. 系统概述 ### 1.1 产品简介 纪念日管理系统是一款面向个人用户的智能纪念日管理工具,主要功能包括纪念日记录与管理、智能提醒通知、便签笔记、AI对话辅助(自然语言解析创建纪念日/提醒)。系统支持多端访问,通过AI技术简化用户操作流程,提供人性化的用户体验。 ### 1.2 部署环境 | 配置项 | 规格 | |-------|------| | 服务器 | 腾讯云 CVM 2核4G | | 操作系统 | Ubuntu 22.04 LTS | | 公网带宽 | 1Mbps | | 数据存储 | 云硬盘 50GB | ### 1.3 开发环境(本地SQLite) | 配置项 | 规格 | |-------|------| | 操作系统 | Windows/macOS/Linux | | 数据库 | SQLite 3 (本地文件) | | 数据库文件 | server/prisma/dev.db | | API服务 | localhost:3000 | | 前端服务 | localhost:5173 | ### 1.4 技术栈概览 ``` 前端层: React 18 + TypeScript + Vite + Mantine UI + Tailwind CSS + Quill 后端层: Node.js + Express + TypeScript + Prisma ORM 数据库: PostgreSQL 14 (生产) / SQLite 3 (本地开发) + Redis 7 AI服务: DeepSeek API 基础设施: Nginx + PM2 + systemd ``` --- ## 2. 整体架构图 ### 2.1 数据流向图 ``` 用户访问 | v +-------------------+ | 腾讯云 CLB | | (负载均衡) | +-------------------+ | v +-------------------+ | Nginx | | SSL终结/反向代理 | +-------------------+ | +-------------------+-------------------+ | | | v v v +-------------+ +-------------+ +-------------+ | 前端静态 | | API代理 | | WebSocket | | (Vite构建) | | /api/* | | (可选) | +-------------+ +-------------+ +-------------+ | v +-------------------+ | Express.js | | 后端服务 | +-------------------+ | +-------------------+-------------------+ | | | v v v +-------------+ +-------------+ +-------------+ | DeepSeek | | Redis | | PostgreSQL | | API | | 缓存层 | | 数据库 | +-------------+ +-------------+ +-------------+ ``` ### 2.2 腾讯云部署架构 ``` Internet | v +---------------------------+ | 腾讯云 CLB (SLB) | 端口: 80/443 +---------------------------+ | v (内网IP: 10.0.0.4) +---------------------------+ | Nginx | SSL证书 + 反向代理 | - qia.example.com | 静态资源服务 +---------------------------+ | +-----> /api/ -------------> localhost:3000 (Express) | +-----> /static -----------> /var/www/app/dist | +-----> /.well-known ------> /var/www/certbot | v +---------------------------+ | PM2 进程管理器 | | - express-api | +---------------------------+ | v +---------------------------+ | 应用层 | | - Controllers | | - Services | | - Middlewares | +---------------------------+ | +-----------+-----------+ | | | v v v +----------+ +----------+ +----------+ | Prisma | | Redis | | DeepSeek | | Client | | Client | | API | +----------+ +----------+ +----------+ | v +---------------------------+ | 数据层 | | PostgreSQL:5432 | | Redis:6379 | +---------------------------+ ``` --- ## 3. 技术选型明细 ### 3.1 前端技术栈 | 技术 | 版本 | 用途 | |-----|------|------| | React | 18.x | UI框架 | | TypeScript | 5.x | 类型安全 | | Vite | 5.x | 构建工具 | | Mantine UI | 7.x | 组件库 | | Tailwind CSS | 3.x | 原子化CSS | | Quill | 2.x | 富文本编辑器 | | Axios | 1.x | HTTP客户端 | | React Router | 6.x | 路由管理 | | Zustand | 4.x | 状态管理 | | Day.js | 1.x | 日期处理 | | Zustand | 4.x | 状态管理 | ### 3.2 后端技术栈 | 技术 | 版本 | 用途 | |-----|------|------| | Node.js | 20.x LTS | 运行时 | | Express | 4.x | Web框架 | | TypeScript | 5.x | 类型安全 | | Prisma | 5.x | ORM框架 | | PostgreSQL | 14.x | 主数据库 | | Redis | 7.x | 缓存/会话 | | JWT | 9.x | 认证令牌 | | bcryptjs | 2.x | 密码加密 | | nodemailer | 6.x | 邮件发送 | | node-cron | 3.x | 定时任务 | | express-rate-limit | 7.x | 请求限流 | | helmet | 7.x | 安全中间件 | | express-validator | 7.x | 请求验证 | | zod | 3.x | Schema验证 | ### 3.3 数据库与基础设施 #### PostgreSQL 配置 ```yaml 主机: localhost 端口: 5432 数据库名: qia_db 用户: qia_user 连接池: min: 2 max: 10 字符集: UTF8 时区: Asia/Shanghai ``` #### Redis 配置 ```yaml 主机: localhost 端口: 6379 密码: ${REDIS_PASSWORD} 数据库: 0 Key过期策略: - AI对话历史: 24小时 - 验证码: 10分钟 - 会话Token: 7天 - 节假日缓存: 24小时 ``` --- ## 4. 数据库设计 ### 4.1 ER图 ``` +----------------+ +-------------------+ +------------------+ | users | | anniversaries | | reminders | +----------------+ +-------------------+ +------------------+ | id (PK) |<----->| id (PK) | | id (PK) | | email (UK) | | user_id (FK) | | anniversary_id(FK)| -| | password_hash | | title | | user_id (FK) | | | nickname | | date | | title | | | avatar | | type | | remind_time | | | email_verified | | is_lunar | | is_completed | | | created_at | | repeat_type | | created_at | | | updated_at | | remind_days | +------------------+ | +----------------+ | notes | | | | created_at | +------------------+ | | | updated_at | | notes | | | +-------------------+ +------------------+ | | | | id (PK) | | | | | user_id (FK) | | | | | title | | | | | content | | | | | color | | | | | position | | | | | created_at | | | | | updated_at | | | | +------------------+ | | | | +-------------------+ | |conversation_history| | +-------------------+ +--------------->| id (PK) | | user_id (FK) | | message_role | | message_content | | created_at | +-------------------+ +----------------+ +-------------------+ |password_resets | | holidays | +----------------+ +-------------------+ | id (PK) | | id (PK) | | user_id (FK) | | name | | token | | date | | expires_at | | type | | used | | created_at | +----------------+ +-------------------+ ``` ### 4.2 表结构详细定义 #### 4.2.1 users 表(用户) ```sql CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, nickname VARCHAR(50) DEFAULT '新用户', avatar TEXT DEFAULT '/default-avatar.png', email_verified BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_created_at ON users(created_at); ``` #### 4.2.2 anniversaries 表(纪念日) ```sql CREATE TABLE anniversaries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(200) NOT NULL, date DATE NOT NULL, type VARCHAR(20) NOT NULL DEFAULT 'other', is_lunar BOOLEAN DEFAULT FALSE, repeat_type VARCHAR(20) DEFAULT 'none', remind_days INTEGER[] DEFAULT '{}', notes TEXT, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_anniversaries_user_id ON anniversaries(user_id); CREATE INDEX idx_anniversaries_date ON anniversaries(date); CREATE INDEX idx_anniversaries_type ON anniversaries(type); CREATE INDEX idx_anniversaries_is_active ON anniversaries(is_active); COMMENT ON COLUMN anniversaries.type IS 'birthday:生日, anniversary:纪念日, holiday:节假日, other:其他'; COMMENT ON COLUMN anniversaries.repeat_type IS 'none:不重复, yearly:每年, monthly:每月, weekly:每周'; COMMENT ON COLUMN anniversaries.remind_days IS '提前提醒天数数组,如[-7, -3, 0]表示提前7天、3天、当天'; ``` #### 4.2.3 reminders 表(提醒) ```sql CREATE TABLE reminders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, type VARCHAR(20) DEFAULT 'reminder' NOT NULL, title VARCHAR(200) NOT NULL, content VARCHAR(500), date TIMESTAMP WITH TIME ZONE NOT NULL, is_lunar INTEGER DEFAULT 0, repeat_type VARCHAR(20) DEFAULT 'none', repeat_interval INTEGER, next_reminder_date TIMESTAMP WITH TIME ZONE, is_completed INTEGER DEFAULT 0, completed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_reminders_user_id ON reminders(user_id); CREATE INDEX idx_reminders_date ON reminders(date); CREATE INDEX idx_reminders_is_completed ON reminders(is_completed); CREATE INDEX idx_reminders_type ON reminders(type); CREATE INDEX idx_reminders_repeat_type ON reminders(repeat_type); COMMENT ON COLUMN reminders.type IS 'reminder:提醒, anniversary:纪念日'; COMMENT ON COLUMN reminders.repeat_type IS 'none:不重复, daily:每天, weekly:每周, monthly:每月, yearly:每年'; COMMENT ON COLUMN reminders.next_reminder_date IS '下一次提醒日期(基于周期计算)'; ``` #### 4.2.4 notes 表(便签) ```sql CREATE TABLE notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(100) NOT NULL, content TEXT, color VARCHAR(20) DEFAULT '#fff9c4', position_x INTEGER DEFAULT 100, position_y INTEGER DEFAULT 100, width INTEGER DEFAULT 200, height INTEGER DEFAULT 200, is_pinned BOOLEAN DEFAULT FALSE, z_index INTEGER DEFAULT 1, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_notes_user_id ON notes(user_id); CREATE INDEX idx_notes_is_pinned ON notes(is_pinned); ``` #### 4.2.5 conversation_history 表(AI对话历史) ```sql CREATE TABLE conversation_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, message_role VARCHAR(20) NOT NULL, message_content TEXT NOT NULL, tokens INTEGER, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_conversation_history_user_id ON conversation_history(user_id); CREATE INDEX idx_conversation_history_created_at ON conversation_history(created_at); COMMENT ON COLUMN conversation_history.message_role IS 'user:用户, assistant:AI'; ``` #### 4.2.6 password_resets 表(密码重置) ```sql CREATE TABLE password_resets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, used BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_password_resets_token ON password_resets(token); CREATE INDEX idx_password_resets_expires_at ON password_resets(expires_at); CREATE INDEX idx_password_resets_used ON password_resets(used); ``` ### 4.3 Prisma Schema 完整实现 ```prisma // e:\qia\server\prisma\schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // 用户模型 model User { id String @id @default(uuid()) email String @unique passwordHash String @map("password_hash") nickname String @default("新用户") avatar String @default("/default-avatar.png") emailVerified Boolean @default(false) @map("email_verified") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") anniversaries Anniversary[] reminders Reminder[] notes Note[] conversations ConversationHistory[] passwordResets PasswordReset[] @@map("users") } // 纪念日模型 model Anniversary { id String @id @default(uuid()) userId String @map("user_id") title String date DateTime type String @default("other") isLunar Boolean @default(false) @map("is_lunar") repeatType String @default("none") @map("repeat_type") remindDays Int[] @map("remind_days") notes String? isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) reminders Reminder[] @@index([userId]) @@index([date]) @@index([type]) @@map("anniversaries") } // 提醒模型 model Reminder { id String @id @default(uuid()) userId String @map("user_id") type String @default("reminder") @map("type") title String content String? @map("content") date DateTime @map("date") isLunar Int @default(0) @map("is_lunar") repeatType String @default("none") @map("repeat_type") repeatInterval Int? @map("repeat_interval") nextReminderDate DateTime? @map("next_reminder_date") isCompleted Int @default(0) @map("is_completed") completedAt DateTime? @map("completed_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([date]) @@index([isCompleted]) @@index([type]) @@index([repeatType]) @@map("reminders") } // 便签模型 model Note { id String @id @default(uuid()) userId String @map("user_id") title String content String? color String @default("#fff9c4") positionX Int @default(100) @map("position_x") positionY Int @default(100) @map("position_y") width Int @default(200) height Int @default(200) isPinned Boolean @default(false) @map("is_pinned") zIndex Int @default(1) @map("z_index") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([isPinned]) @@map("notes") } // AI对话历史模型 model ConversationHistory { id String @id @default(uuid()) userId String @map("user_id") messageRole String @map("message_role") messageContent String @map("message_content") tokens Int? createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([createdAt]) @@map("conversation_history") } // 密码重置模型 model PasswordReset { id String @id @default(uuid()) userId String @map("user_id") token String @unique expiresAt DateTime @map("expires_at") used Boolean @default(false) createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([token]) @@index([expiresAt]) @@map("password_resets") } // 节假日缓存模型 model Holiday { id String @id @default(uuid()) name String date DateTime @unique type String createdAt DateTime @default(now()) @map("created_at") @@map("holidays") } ``` --- ## 5. API接口设计 ### 5.1 认证模块 #### 5.1.1 用户注册 ``` POST /api/auth/register Content-Type: application/json Request: { "email": "user@example.com", "password": "SecureP@ss123", "nickname": "用户名" } Response (201): { "success": true, "message": "注册成功,请查收验证邮件", "data": { "userId": "uuid-string" } } Response (400): { "success": false, "error": "VALIDATION_ERROR", "message": "邮箱格式不正确" } ``` #### 5.1.2 用户登录 ``` POST /api/auth/login Content-Type: application/json Request: { "email": "user@example.com", "password": "SecureP@ss123" } Response (200): { "success": true, "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "expiresIn": 3600, "user": { "id": "uuid-string", "email": "user@example.com", "nickname": "用户名", "avatar": "/default-avatar.png" } } } Response (401): { "success": false, "error": "INVALID_CREDENTIALS", "message": "邮箱或密码错误" } ``` #### 5.1.3 验证邮箱 ``` GET /api/auth/verify-email/:token Response (200): { "success": true, "message": "邮箱验证成功" } ``` #### 5.1.4 请求密码重置 ``` POST /api/auth/forgot-password Content-Type: application/json Request: { "email": "user@example.com" } Response (200): { "success": true, "message": "如果该邮箱已注册,我们将发送密码重置链接" } ``` #### 5.1.5 重置密码 ``` POST /api/auth/reset-password Content-Type: application/json Request: { "token": "reset-token-from_email", "password": "NewSecureP@ss123" } Response (200): { "success": true, "message": "密码重置成功" } ``` #### 5.1.6 刷新Token ``` POST /api/auth/refresh Authorization: Bearer {refreshToken} Response (200): { "success": true, "data": { "accessToken": "new_access_token", "expiresIn": 3600 } } ``` ### 5.2 纪念日模块 #### 5.2.1 创建纪念日 ``` POST /api/anniversaries Authorization: Bearer {accessToken} Content-Type: application/json Request: { "title": "结婚纪念日", "date": "2020-01-15", "type": "anniversary", "isLunar": false, "repeatType": "yearly", "remindDays": [-7, -3, 0], "notes": "重要的日子" } Response (201): { "success": true, "data": { "id": "uuid-string", "title": "结婚纪念日", "date": "2020-01-15T00:00:00Z", "type": "anniversary", "nextRemindDate": "2026-01-08T00:00:00Z" } } ``` #### 5.2.2 获取纪念日列表 ``` GET /api/anniversaries?type=all&active=true&page=1&limit=20 Authorization: Bearer {accessToken} Response (200): { "success": true, "data": { "items": [ { "id": "uuid-string", "title": "结婚纪念日", "date": "2020-01-15", "type": "anniversary", "isLunar": false, "repeatType": "yearly", "remindDays": [-7, -3, 0], "daysUntilNext": 353, "upcomingReminder": { "id": "uuid-string", "remindTime": "2026-01-08T09:00:00Z" } } ], "pagination": { "page": 1, "limit": 20, "total": 15, "totalPages": 1 } } } ``` #### 5.2.3 获取单个纪念日 ``` GET /api/anniversaries/:id Authorization: Bearer {accessToken} Response (200): { "success": true, "data": { "id": "uuid-string", "title": "结婚纪念日", "date": "2020-01-15", "type": "anniversary", "isLunar": false, "repeatType": "yearly", "remindDays": [-7, -3, 0], "notes": "重要的日子", "createdAt": "2024-01-15T10:00:00Z" } } ``` #### 5.2.4 更新纪念日 ``` PUT /api/anniversaries/:id Authorization: Bearer {accessToken} Content-Type: application/json Request: { "title": "结婚纪念日 - 更新", "remindDays": [-14, -7, -3, 0] } Response (200): { "success": true, "message": "纪念日更新成功" } ``` #### 5.2.5 删除纪念日 ``` DELETE /api/anniversaries/:id Authorization: Bearer {accessToken} Response (200): { "success": true, "message": "纪念日删除成功" } ``` #### 5.2.6 获取内置节假日 ``` GET /api/holidays?year=2026 Authorization: Bearer {accessToken} Response (200): { "success": true, "data": { "year": 2026, "holidays": [ { "name": "元旦", "date": "2026-01-01", "type": "holiday" }, { "name": "春节", "date": "2026-02-17", "type": "holiday" } ] } } ``` ### 5.3 提醒模块 #### 5.3.1 获取提醒列表(按状态分组) ``` GET /api/reminders?status=pending&upcomingDays=7 Authorization: Bearer {accessToken} Response (200): { "success": true, "data": { "pending": [ { "id": "uuid-string", "title": "结婚纪念日提醒", "remindTime": "2026-01-08T09:00:00Z", "daysUntil": 7, "anniversaryTitle": "结婚纪念日" } ], "completed": [ { "id": "uuid-string", "title": "生日提醒", "remindTime": "2025-12-25T09:00:00Z", "completedAt": "2025-12-25T08:30:00Z" } ], "expired": [] } } ``` #### 5.3.2 标记提醒完成 ``` PATCH /api/reminders/:id/complete Authorization: Bearer {accessToken} Response (200): { "success": true, "message": "提醒已标记为完成" } ``` #### 5.3.3 标记提醒未完成 ``` PATCH /api/reminders/:id/uncomplete Authorization: Bearer {accessToken} Response (200): { "success": true, "message": "提醒已标记为未完成" } ``` #### 5.3.4 删除提醒 ``` DELETE /api/reminders/:id Authorization: Bearer {accessToken} Response (200): { "success": true, "message": "提醒删除成功" } ``` ### 5.4 便签模块 #### 5.4.1 获取便签列表 ``` GET /api/notes Authorization: Bearer {accessToken} Response (200): { "success": true, "data": [ { "id": "uuid-string", "title": "购物清单", "content": "牛奶、面包、鸡蛋", "color": "#fff9c4", "positionX": 100, "positionY": 100, "width": 200, "height": 200, "isPinned": true, "zIndex": 2 } ] } ``` #### 5.4.2 创建便签 ``` POST /api/notes Authorization: Bearer {accessToken} Content-Type: application/json Request: { "title": "新便签", "content": "便签内容", "color": "#e3f2fd", "positionX": 150, "positionY": 150 } Response (201): { "success": true, "data": { "id": "uuid-string", "title": "新便签", "content": "便签内容", "color": "#e3f2fd", "positionX": 150, "positionY": 150 } } ``` #### 5.4.3 更新便签位置/尺寸 ``` PATCH /api/notes/:id Authorization: Bearer {accessToken} Content-Type: application/json Request: { "positionX": 200, "positionY": 200, "width": 250, "height": 250 } Response (200): { "success": true, "message": "便签更新成功" } ``` #### 5.4.4 删除便签 ``` DELETE /api/notes/:id Authorization: Bearer {accessToken} Response (200): { "success": true, "message": "便签删除成功" } ``` ### 5.5 AI模块(自然语言解析) #### 5.5.1 自然语言创建纪念日/提醒 ``` POST /api/ai/parse Authorization: Bearer {accessToken} Content-Type: application/json Request: { "message": "提醒我3天后是妈妈的生日,提前7天和3天提醒我", "context": "create_anniversary" } Response (200): { "success": true, "data": { "parsed": { "action": "create_anniversary", "title": "妈妈的生日", "date": "2026-02-14", "isLunar": false, "type": "birthday", "repeatType": "yearly", "remindDays": [-7, -3, 0] }, "confirmNeeded": true, "confirmMessage": "我理解您想创建生日提醒,对吗?详情:纪念日「妈妈的生日」,日期2026年2月14日,每年重复,提前7天、3天、当天提醒。" } } ``` #### 5.5.2 确认创建 ``` POST /api/ai/confirm Authorization: Bearer {accessToken} Content-Type: application/json Request: { "parsedData": { "action": "create_anniversary", "title": "妈妈的生日", "date": "2026-02-14", "isLunar": false, "type": "birthday", "repeatType": "yearly", "remindDays": [-7, -3, 0] } } Response (201): { "success": true, "data": { "anniversaryId": "uuid-string", "message": "已成功创建生日纪念日「妈妈的生日」" } } ``` #### 5.5.3 AI对话 ``` POST /api/ai/chat Authorization: Bearer {accessToken} Content-Type: application/json Request: { "message": "下个月有什么重要的纪念日?", "historyId": "uuid-string" // 可选,上下文ID } Response (200): { "success": true, "data": { "reply": "根据您的记录,下个月有以下纪念日:\n1. 2月14日 - 结婚纪念日(还有15天)\n2. 2月21日 - 父亲的生日(还有22天)", "historyId": "new-or-existing-uuid" } } ``` --- ## 6. 目录结构 ### 6.1 前端项目结构 ``` e:\qia\client\ ├── public/ │ ├── favicon.ico │ └── robots.txt ├── src/ │ ├── assets/ │ │ ├── images/ │ │ └── styles/ │ │ └── index.css # Tailwind入口 │ ├── components/ │ │ ├── anniversary/ │ │ │ ├── AnniversaryCard/ │ │ │ ├── AnniversaryForm/ │ │ │ ├── AnniversaryList/ │ │ │ └── HolidayCard/ │ │ ├── reminder/ │ │ │ ├── ReminderCard/ │ │ │ ├── ReminderList/ │ │ │ ├── ReminderForm/ │ │ │ ├── ArchiveReminderModal/ │ │ │ └── ReminderDetailModal/ │ │ ├── note/ │ │ │ ├── NoteCard/ │ │ │ └── NoteEditor/ │ │ ├── ai/ │ │ │ └── ChatBox/ │ │ ├── layout/ │ │ │ └── Layout/ │ │ └── common/ │ │ ├── Modal/ │ │ └── Toast/ │ ├── pages/ │ │ ├── Home/ │ │ ├── Dashboard/ │ │ ├── Auth/ │ │ │ ├── Login/ │ │ │ ├── Register/ │ │ │ └── ForgotPassword/ │ │ ├── Anniversary/ │ │ │ ├── List/ │ │ │ ├── Detail/ │ │ │ └── Create/ │ │ ├── Reminder/ │ │ │ └── List/ │ │ ├── Note/ │ │ │ └── Canvas/ │ │ ├── AI/ │ │ │ └── Chat/ │ │ └── Settings/ │ │ ├── Profile/ │ │ ├── Security/ │ │ └── Notifications/ │ ├── hooks/ │ │ ├── useAuth.ts │ │ ├── useAnniversary.ts │ │ ├── useReminder.ts │ │ ├── useNote.ts │ │ ├── useAI.ts │ │ ├── useDebounce.ts │ │ └── useLocalStorage.ts │ ├── services/ │ │ ├── api/ │ │ │ ├── client.ts # Axios实例 │ │ │ ├── endpoints.ts # API端点定义 │ │ │ ├── authService.ts │ │ │ ├── anniversaryService.ts │ │ │ ├── reminderService.ts │ │ │ ├── noteService.ts │ │ │ └── aiService.ts │ │ └── socket/ │ │ └── socketService.ts │ ├── stores/ │ │ └── index.ts # Zustand主状态管理(包含所有store) │ ├── utils/ │ │ ├── validators.ts │ │ ├── formatters.ts │ │ ├── dateUtils.ts │ │ ├── constants.ts │ │ └── helpers.ts │ ├── types/ │ │ ├── user.ts │ │ ├── anniversary.ts │ │ ├── reminder.ts │ │ ├── note.ts │ │ ├── ai.ts │ │ └── api.ts │ ├── App.tsx │ ├──│ └── vite-env.d.ts main.tsx ├── .env ├── .env.example ├── .eslintrc.cjs ├── .prettierrc ├── index.html ├── package.json ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts ``` ### 6.2 后端项目结构 ``` e:\qia\server\ ├── prisma/ │ ├── migrations/ │ └── schema.prisma ├── src/ │ ├── config/ │ │ ├── index.ts # 环境变量加载 │ │ ├── database.ts # 数据库配置 │ │ └── redis.ts # Redis配置 │ ├── controllers/ │ │ ├── authController.ts │ │ ├── anniversaryController.ts │ │ ├── reminderController.ts │ │ ├── noteController.ts │ │ ├── aiController.ts │ │ └── holidayController.ts │ ├── services/ │ │ ├── authService.ts │ │ ├── anniversaryService.ts │ │ ├── reminderService.ts │ │ ├── noteService.ts │ │ ├── aiService.ts │ │ ├── holidayService.ts │ │ ├── emailService.ts │ │ └── cronService.ts │ ├── routes/ │ │ ├── authRoutes.ts │ │ ├── anniversaryRoutes.ts │ │ ├── reminderRoutes.ts │ │ ├── noteRoutes.ts │ │ ├── aiRoutes.ts │ │ └── holidayRoutes.ts │ ├── middlewares/ │ │ ├── authMiddleware.ts │ │ ├── rateLimitMiddleware.ts │ │ ├── errorMiddleware.ts │ │ ├── validationMiddleware.ts │ │ └── securityMiddleware.ts │ ├── validators/ │ │ ├── authValidator.ts │ │ ├── anniversaryValidator.ts │ │ ├── reminderValidator.ts │ │ ├── noteValidator.ts │ │ └── aiValidator.ts │ ├── types/ │ │ ├── express.d.ts │ │ └── custom.d.ts │ ├── utils/ │ │ ├── jwt.ts │ │ ├── password.ts │ │ ├── dateUtils.ts │ │ └── response.ts │ ├── app.ts │ └── server.ts ├── .env ├── .env.example ├── .eslintrc.cjs ├── .prettierrc ├── package.json ├── tsconfig.json └── jest.config.js ``` --- ## 7. 关键实现方案 ### 7.1 认证流程 #### 7.1.1 注册验证流程 ``` 用户提交注册信息 | v 验证邮箱格式 + 密码强度 | +-- 失败 --> 返回错误 | v 检查邮箱是否已注册 | +-- 已存在 --> 返回错误 | v 生成邮箱验证Token (JWT, 24小时过期) | v 发送验证邮件 | +-- 发送失败 --> 返回错误 | v 创建用户记录 (email_verified = false) | v 返回成功响应 ``` #### 7.1.2 邮箱验证流程 ``` 用户点击邮件中的验证链接 | v 解析Token (验证签名 + 过期时间) | +-- 无效/过期 --> 返回错误页面 | v 查询对应用户 | +-- 不存在 --> 返回错误 | v 更新用户 email_verified = true | v 返回验证成功页面 ``` #### 7.1.3 密码重置流程 ``` 用户请求密码重置 | v 验证邮箱是否存在 | +-- 不存在 --> 返回通用消息 | v 生成重置Token (JWT, 30分钟过期) | v 保存Token到数据库 | v 发送重置邮件 | v 用户点击邮件链接 | v 验证Token + 检查是否已使用 | +-- 无效/已使用 --> 返回错误 | v 显示重置密码表单 | v 提交新密码 | v 验证密码强度 | v 更新密码Hash | v 标记Token为已使用 | v 返回成功 ``` #### 7.1.4 Token刷新机制 ``` Access Token过期 | v 检查Refresh Token | +-- 无效 --> 跳转登录 | v 验证Refresh Token签名 | v 查询用户状态 | +-- 用户禁用 --> 跳转登录 | v 生成新的Access Token | v 返回新Token ``` ### 7.2 后端定时任务 #### 7.2.1 定时任务配置 (node-cron) ```typescript // src/services/cronService.ts import cron from 'node-cron'; import { reminderService } from './reminderService'; import { emailService } from './emailService'; import { anniversaryService } from './anniversaryService'; class CronService { constructor() { this.scheduleJobs(); } private scheduleJobs() { // 每小时检查一次即将到来的提醒 (每小时第5分钟执行) cron.schedule('5 * * * *', async () => { console.log('[Cron] 检查即将到来的提醒...'); await this.checkUpcomingReminders(); }); // 每天凌晨2点生成过期提醒汇总 cron.schedule('0 2 * * *', async () => { console.log('[Cron] 生成每日提醒汇总...'); await this.generateDailySummary(); }); // 每天凌晨3点清理过期的密码重置Token cron.schedule('0 3 * * *', async () => { console.log('[Cron] 清理过期Token...'); await this.cleanupExpiredTokens(); }); // 每周日凌晨4点更新节假日数据 cron.schedule('0 4 * * 0', async () => { console.log('[Cron] 更新节假日数据...'); await this.updateHolidays(); }); // 每月1号凌晨1点VACUUM数据库 cron.schedule('0 1 1 * *', async () => { console.log('[Cron] 数据库维护...'); await this.maintainDatabase(); }); } private async checkUpcomingReminders() { try { const upcomingReminders = await reminderService.getUpcomingReminders(24); for (const reminder of upcomingReminders) { const user = await this.getUserById(reminder.userId); if (user && user.emailVerified) { await emailService.sendReminderEmail(user.email, { title: reminder.title, remindTime: reminder.remindTime, anniversaryTitle: reminder.anniversary?.title, }); // 标记为已发送 await reminderService.markAsNotified(reminder.id); } } } catch (error) { console.error('[Cron] 检查提醒失败:', error); } } private async generateDailySummary() { try { const tomorrowReminders = await reminderService.getRemindersByDate( new Date(Date.now() + 24 * 60 * 60 * 1000) ); // 发送给用户汇总邮件 } catch (error) { console.error('[Cron] 生成汇总失败:', error); } } private async cleanupExpiredTokens() { // 清理过期的密码重置Token } private async updateHolidays() { // 更新节假日缓存 } private async maintainDatabase() { // 执行VACUUM ANALYZE } } export const cronService = new CronService(); ``` ### 7.3 DeepSeek API集成 #### 7.3.1 Prompt设计 ```typescript // src/services/aiService.ts const SYSTEM_PROMPT = `你是一个智能纪念日管理助手。你的任务是: 1. 解析用户输入的自然语言,提取纪念日相关信息 2. 返回结构化的JSON格式结果 支持的纪念日类型: - birthday: 生日 - anniversary: 纪念日 - holiday: 节假日 - other: 其他 重复类型: - none: 不重复 - yearly: 每年 - monthly: 每月 - weekly: 每周 输出格式必须是有效的JSON对象,格式如下: { "action": "create_anniversary" | "create_reminder" | "query" | "chat", "title": "纪念日标题", "date": "YYYY-MM-DD格式的日期", "type": "birthday | anniversary | holiday | other", "isLunar": true | false, "repeatType": "none | yearly | monthly | weekly", "remindDays": [-14, -7, -3, 0], "message": "对用户的回复消息" } 请根据用户输入智能判断日期和提醒设置。`; const PARSE_EXAMPLE = ` 用户输入:"提醒我3天后是妈妈的生日,提前7天和3天提醒我" 解析结果: { "action": "create_anniversary", "title": "妈妈的生日", "date": "2026-02-14", "type": "birthday", "isLunar": false, "repeatType": "yearly", "remindDays": [-7, -3, 0], "message": "我理解您想创建妈妈的生日纪念日,对吗?\n详情:\n- 标题:妈妈的生日\n- 日期:2026年2月14日\n- 重复:每年\n- 提醒:提前7天、3天、当天" } `; ``` #### 7.3.2 JSON解析服务 ```typescript import OpenAI from 'openai'; class AIService { private client: OpenAI; constructor() { this.client = new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: 'https://api.deepseek.com', }); } async parseNaturalLanguage(userId: string, message: string) { const completion = await this.client.chat.completions.create({ model: 'deepseek-chat', messages: [ { role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: message }, ], temperature: 0.1, response_format: { type: 'json_object' }, }); const result = JSON.parse(completion.choices[0].message.content); // 保存对话历史 await this.saveConversationHistory(userId, 'user', message); await this.saveConversationHistory(userId, 'assistant', result.message); return result; } async chat(userId: string, message: string, historyId?: string) { const messages = await this.getConversationHistory(userId, historyId); messages.push({ role: 'user', content: message }); const completion = await this.client.chat.completions.create({ model: 'deepseek-chat', messages, temperature: 0.7, }); const reply = completion.choices[0].message.content; // 保存对话 const newHistoryId = await this.saveConversationHistory(userId, 'user', message, reply); return { reply, historyId: newHistoryId }; } private async saveConversationHistory( userId: string, role: 'user' | 'assistant', content: string, reply?: string ): Promise { // 存入数据库 } private async getConversationHistory(userId: string, historyId?: string) { // 获取历史消息 } } export const aiService = new AIService(); ``` ### 7.4 节假日API集成 #### 7.4.1 动态获取与缓存策略 ```typescript // src/services/holidayService.ts import Redis from 'ioredis'; import axios from 'axios'; const redis = new Redis(process.env.REDIS_URL); const HOLIDAY_API = 'https://api.annapi.cn/holidays'; class HolidayService { // 缓存Key private getCacheKey(year: number) => `holidays:${year}`; // 获取节假日数据 async getHolidays(year: number) { const cacheKey = this.getCacheKey(year); // 尝试从Redis获取 const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // 从API获取 const response = await axios.get(`${HOLIDAY_API}/${year}`); const holidays = response.data; // 缓存到Redis (24小时) await redis.setex(cacheKey, 86400, JSON.stringify(holidays)); // 保存到数据库 await this.saveToDatabase(holidays); return holidays; } // 保存到数据库 private async saveToDatabase(holidays: any[]) { // 使用Prisma批量创建 } // 获取所有节假日(从数据库) async getHolidaysFromDB(year: number) { // 从PostgreSQL查询 } // 搜索节假日 async searchHolidays(query: string, year: number) { const holidays = await this.getHolidays(year); return holidays.filter(h => h.name.includes(query) || h.date.includes(query) ); } } export const holidayService = new HolidayService(); ``` --- ## 8. 腾讯云部署方案 ### 8.1 Nginx配置 ```nginx # /etc/nginx/sites-available/qia-app server { listen 80; server_name qia.example.com; # 重定向到HTTPS return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name qia.example.com; # SSL证书配置 ssl_certificate /etc/letsencrypt/live/qia.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/qia.example.com/privkey.pem; ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_session_tickets off; # SSL安全配置 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # 添加安全头 add_header Strict-Transport-Security "max-age=63072000" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; # 静态资源目录 root /var/www/qia-app/dist; index index.html; # 前端路由支持 location / { try_files $uri $uri/ /index.html; } # API代理 location /api/ { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; # 超时设置 proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } # Let's Encrypt验证 location /.well-known/acme-challenge/ { root /var/www/certbot; } } ``` ### 8.2 PM2进程管理配置 ```javascript // ecosystem.config.js module.exports = { apps: [ { name: 'express-api', script: './dist/server.js', cwd: '/var/www/qia-app/server', instances: 1, autorestart: true, watch: false, max_memory_restart: '500M', env: { NODE_ENV: 'production', PORT: 3000, }, env_file: '.env', error_file: '/var/log/pm2/express-api-error.log', out_file: '/var/log/pm2/express-api-out.log', log_file: '/var/log/pm2/express-api-combined.log', time: true, }, ], }; ``` PM2常用命令: ```bash # 启动应用 pm2 start ecosystem.config.js # 查看状态 pm2 status # 查看日志 pm2 logs express-api # 重启 pm2 restart express-api # 停止 pm2 stop express-api # 删除 pm2 delete express-api # 开机自启 pm2 startup pm2 save ``` ### 8.3 SSL证书自动续期 ```bash # /etc/cron.d/certbot-renew # 每周一凌晨4点尝试续期 0 4 * * 1 root certbot renew --quiet && nginx -s reload ``` 续期脚本: ```bash #!/bin/bash # /var/www/certbot/renew.sh certbot renew --quiet if [ $? -eq 0 ]; then nginx -s reload echo "证书已续期,Nginx已重载" fi ``` ### 8.4 systemd服务配置 ```ini # /etc/systemd/system/qia-app.service [Unit] Description=Qia Anniversary Management App After=network.target postgresql.service redis-server.service [Service] Type=forking User=www-data Group=www-data WorkingDirectory=/var/www/qia-app/server ExecStart=/usr/bin/pm2 resurrect ExecStop=/usr/bin/pm2 kill ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=5 # 环境变量 Environment=NODE_ENV=production EnvironmentFile=/var/www/qia-app/.env # 日志 StandardOutput=syslog StandardError=syslog SyslogIdentifier=qia-app [Install] WantedBy=multi-user.target ``` ```bash # 启用服务 sudo systemctl daemon-reload sudo systemctl enable qia-app sudo systemctl start qia-app # 查看状态 sudo systemctl status qia-app # 查看日志 sudo journalctl -u qia-app -f ``` --- ## 9. 安全策略 ### 9.1 JWT认证中间件 ```typescript // src/middlewares/authMiddleware.ts import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); const JWT_SECRET = process.env.JWT_SECRET!; const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!; interface TokenPayload { userId: string; email: string; type: 'access' | 'refresh'; } declare global { namespace Express { interface Request { user?: { id: string; email: string; }; } } } export const authMiddleware = async ( req: Request, res: Response, next: NextFunction ): Promise => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { res.status(401).json({ success: false, error: 'UNAUTHORIZED', message: '未提供认证令牌', }); return; } const token = authHeader.split(' ')[1]; try { const decoded = jwt.verify(token, JWT_SECRET) as TokenPayload; // 检查用户是否存在且未禁用 const user = await prisma.user.findUnique({ where: { id: decoded.userId }, select: { id: true, email: true }, }); if (!user) { res.status(401).json({ success: false, error: 'USER_NOT_FOUND', message: '用户不存在', }); return; } req.user = { id: user.id, email: user.email }; next(); } catch (jwtError) { if (jwtError instanceof jwt.TokenExpiredError) { res.status(401).json({ success: false, error: 'TOKEN_EXPIRED', message: '认证令牌已过期', }); } else { res.status(401).json({ success: false, error: 'INVALID_TOKEN', message: '无效的认证令牌', }); } } } catch (error) { next(error); } }; export const optionalAuthMiddleware = async ( req: Request, res: Response, next: NextFunction ): Promise => { const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { await authMiddleware(req, res, next); } else { next(); } }; ``` ### 9.2 请求限流 ```typescript // src/middlewares/rateLimitMiddleware.ts import rateLimit from 'express-rate-limit'; import { Request, Response } from 'express'; // 通用API限流 export const apiRateLimit = rateLimit({ windowMs: 15 * 60 * 1000, // 15分钟 max: 100, // 100次请求 message: { success: false, error: 'RATE_LIMIT_EXCEEDED', message: '请求过于频繁,请稍后再试', }, standardHeaders: true, legacyHeaders: false, }); // 登录限流 export const loginRateLimit = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, // 15分钟内最多5次登录尝试 message: { success: false, error: 'LOGIN_RATE_LIMIT', message: '登录尝试次数过多,请15分钟后重试', }, skipSuccessfulRequests: true, }); // 验证码限流 export const captchaRateLimit = rateLimit({ windowMs: 60 * 60 * 1000, // 1小时 max: 10, // 最多10次请求 message: { success: false, error: 'CAPTCHA_RATE_LIMIT', message: '验证码请求过于频繁', }, }); // AI API限流 export const aiRateLimit = rateLimit({ windowMs: 60 * 1000, // 1分钟 max: 20, // 每分钟20次 message: { success: false, error: 'AI_RATE_LIMIT', message: 'AI请求过于频繁,请稍后再试', }, keyGenerator: (req: Request) => req.user?.id || req.ip, }); // 注册限流 export const registerRateLimit = rateLimit({ windowMs: 60 * 60 * 1000, // 1小时 max: 5, // 最多5次注册 message: { success: false, error: 'REGISTER_RATE_LIMIT', message: '注册过于频繁,请稍后再试', }, }); ``` ### 9.3 安全中间件 ```typescript // src/middlewares/securityMiddleware.ts import helmet from 'helmet'; import cors from 'cors'; import { Request, Response, NextFunction } from 'express'; // Helmet安全头 export const securityMiddleware = helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", "https://api.deepseek.com"], }, }, crossOriginEmbedderPolicy: false, }); // CORS配置 export const corsMiddleware = cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || ['https://qia.example.com'], credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], exposedHeaders: ['X-Total-Count'], maxAge: 86400, // 24小时 }); // XSS防护 (在HTML输出时自动转义) export const xssMiddleware = (req: Request, res: Response, next: NextFunction) => { // Express默认会对路由参数进行一些XSS防护 // 额外的:在JSON响应中转义特殊字符 const originalJson = res.json; res.json = (body: any) => { const sanitized = sanitizeObject(body); return originalJson.call(res, sanitized); }; next(); }; function sanitizeObject(obj: any): any { if (typeof obj === 'string') { return obj .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } if (Array.isArray(obj)) { return obj.map(sanitizeObject); } if (typeof obj === 'object' && obj !== null) { const result: any = {}; for (const key in obj) { result[key] = sanitizeObject(obj[key]); } return result; } return obj; } ``` ### 9.4 密码强度验证 ```typescript // src/validators/authValidator.ts import { body } from 'express-validator'; import { z } from 'zod'; // 密码验证正则 const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; export const passwordValidation = body('password') .isLength({ min: 8, max: 128 }) .withMessage('密码长度必须在8-128个字符之间') .matches(PASSWORD_REGEX) .withMessage('密码必须包含大小写字母、数字和特殊字符'); // Zod Schema验证 export const registerSchema = z.object({ email: z.string().email('邮箱格式不正确'), password: z .string() .min(8, '密码至少8个字符') .regex(/[A-Z]/, '密码必须包含大写字母') .regex(/[a-z]/, '密码必须包含小写字母') .regex(/\d/, '密码必须包含数字') .regex(/[@$!%*?&]/, '密码必须包含特殊字符'), nickname: z .string() .min(2, '昵称至少2个字符') .max(50, '昵称最多50个字符') .regex(/^[\u4e00-\u9fa5a-zA-Z0-9_]+$/, '昵称只能包含中文、英文、数字和下划线'), }); // 密码强度检查函数 export const checkPasswordStrength = (password: string): { score: number; feedback: string[]; } => { const feedback: string[] = []; let score = 0; if (password.length >= 8) score++; else feedback.push('密码长度至少8个字符'); if (/[A-Z]/.test(password)) score++; else feedback.push('需要包含大写字母'); if (/[a-z]/.test(password)) score++; else feedback.push('需要包含小写字母'); if (/\d/.test(password)) score++; else feedback.push('需要包含数字'); if (/[@$!%*?&]/.test(password)) score++; else feedback.push('需要包含特殊字符'); return { score, feedback }; }; ``` --- ## 10. 性能优化建议 ### 10.1 前端优化 #### 10.1.1 代码分割与懒加载 ```typescript // src/App.tsx import { Suspense, lazy } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { LoadingSpinner } from './components/common/Loading'; // 懒加载页面组件 const Home = lazy(() => import('./pages/Home')); const Login = lazy(() => import('./pages/Auth/Login')); const Register = lazy(() => import('./pages/Auth/Register')); const Dashboard = lazy(() => import('./pages/Dashboard')); const AnniversaryList = lazy(() => import('./pages/Anniversary/List')); const NoteCanvas = lazy(() => import('./pages/Note/Canvas')); function App() { return ( }> } /> } /> } /> } /> } /> } /> ); } export default App; ``` #### 10.1.2 缓存策略 ```typescript // 使用React Query进行数据缓存 import { useQuery, useMutation, useQueryClient } from '@react-query'; const useAnniversaries = (userId: string) => { return useQuery({ queryKey: ['anniversaries', userId], queryFn: () => anniversaryService.getList(userId), staleTime: 5 * 60 * 1000, // 5分钟内数据视为新鲜 cacheTime: 30 * 60 * 1000, // 30分钟后清理缓存 }); }; ``` #### 10.1.3 静态资源CDN ```typescript // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { rollupOptions: { output: { manualChunks: { 'vendor-react': ['react', 'react-dom', 'react-router-dom'], 'vendor-mantine': ['@mantine/core', '@mantine/hooks'], 'vendor-ui': ['@mantine/core', '@mantine/hooks', '@mantine/dates'], }, }, }, }, }); ``` ### 10.2 后端优化 #### 10.2.1 数据库连接池 ```typescript // src/config/database.ts import { PrismaClient } from '@prisma/client'; const prismaClientSingleton = () => { return new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], datasources: { db: { url: process.env.DATABASE_URL, }, }, }); }; type PrismaClientSingleton = ReturnType; const globalForPrisma = globalThis as unknown as { prisma: PrismaClientSingleton | undefined; }; export const prisma = globalForPrisma.prisma ?? prismaClientSingleton(); if (process.env.NODE_ENV !== 'production') { globalForPrisma.prisma = prisma; } // 手动管理连接池 prisma.$connect().then(() => { console.log('数据库连接成功'); }); // 优雅关闭 process.on('SIGINT', async () => { await prisma.$disconnect(); process.exit(0); }); ``` #### 10.2.2 Redis缓存层 ```typescript // src/utils/cache.ts import Redis from 'ioredis'; import { promisify } from 'util'; const redis = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379'), password: process.env.REDIS_PASSWORD, db: 0, retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); return delay; }, }); class CacheService { // 获取缓存 async get(key: string): Promise { const value = await redis.get(key); return value ? JSON.parse(value) : null; } // 设置缓存 async set(key: string, value: any, ttlSeconds: number = 3600): Promise { await redis.setex(key, ttlSeconds, JSON.stringify(value)); } // 删除缓存 async del(key: string): Promise { await redis.del(key); } // 批量删除 async delPattern(pattern: string): Promise { const keys = await redis.keys(pattern); if (keys.length > 0) { await redis.del(...keys); } } // 检查键是否存在 async exists(key: string): Promise { return (await redis.exists(key)) === 1; } // 增量操作 async incr(key: string): Promise { return await redis.incr(key); } } export const cacheService = new CacheService(); ``` #### 10.2.3 异步处理 ```typescript // 使用消息队列处理非实时任务 import { Queue, Worker } from 'bull'; import Redis from 'ioredis'; const redis = new Redis(process.env.REDIS_URL); // 邮件队列 const emailQueue = new Queue('email', { redis: redis.duplicate() }); // 添加到队列 export const sendReminderEmailJob = async (data: { email: string; title: string; remindTime: Date; }) => { await emailQueue.add('send-reminder', data, { delay: 1000, // 延迟1秒执行 attempts: 3, backoff: 5000, }); }; // 邮件发送Worker const emailWorker = new Worker( 'email', async (job) => { const { email, title, remindTime } = job.data; await emailService.sendReminderEmail(email, { title, remindTime }); }, { concurrency: 5 } ); emailWorker.on('completed', (job) => { console.log(`邮件发送完成: ${job.id}`); }); emailWorker.on('failed', (job, err) => { console.error(`邮件发送失败: ${job?.id}`, err); }); ``` ### 10.3 数据库维护 #### 10.3.1 定期维护脚本 ```sql -- 每月执行的维护脚本 -- 保存为 maintenance.sql -- 1. 清理旧对话历史(保留最近3个月) DELETE FROM conversation_history WHERE created_at < NOW() - INTERVAL '3 months'; -- 2. 清理已使用的密码重置Token DELETE FROM password_resets WHERE used = true OR expires_at < NOW(); -- 3. 更新统计信息 VACUUM ANALYZE users; VACUUM ANALYZE anniversaries; VACUUM ANALYZE reminders; VACUUM ANALYZE notes; VACUUM ANALYZE conversation_history; -- 4. 检查索引碎片 SELECT indexrelname, idx_scan, idx_tup_read, idx_tup_fetch, pg_size_pretty(pg_relation_size(indexrelname::regclass)) as index_size FROM pg_stat_user_indexes WHERE schemaname = 'public' ORDER BY idx_scan ASC; ``` #### 10.3.2 慢查询分析 ```sql -- 开启慢查询日志(需要重启数据库) ALTER DATABASE qia_db SET log_min_duration_statement = 1000; -- 查询最近慢查询 SELECT query, calls, mean_time, total_time, rows FROM pg_stat_statements ORDER BY mean_time DESC LIMIT 20; -- 查询频繁查询 SELECT query, calls, total_time, rows FROM pg_stat_statements ORDER BY calls DESC LIMIT 20; -- 分析查询计划 EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT * FROM anniversaries WHERE user_id = 'uuid-here' ORDER BY date DESC; ``` --- ## 附录 ### A. 环境变量配置 ```env # .env.example # 应用配置 NODE_ENV=production PORT=3000 CLIENT_URL=https://qia.example.com # 数据库配置 DATABASE_URL=postgresql://qia_user:password@localhost:5432/qia_db?schema=public # Redis配置 REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=your_redis_password # JWT配置 JWT_SECRET=your_jwt_access_secret_min_32_chars JWT_REFRESH_SECRET=your_jwt_refresh_secret_min_32_chars JWT_ACCESS_EXPIRES=3600 JWT_REFRESH_EXPIRES=604800 # 邮件配置 SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USER=noreply@qia.example.com SMTP_PASSWORD=your_smtp_password EMAIL_FROM=noreply@qia.example.com # DeepSeek API配置 DEEPSEEK_API_KEY=your_deepseek_api_key # 腾讯云配置 TENCENT_CLOUD_SECRET_ID=your_secret_id TENCENT_CLOUD_SECRET_KEY=your_secret_key # 安全配置 BCRYPT_ROUNDS=12 ``` ### B. API响应格式规范 ```typescript // 成功响应 interface SuccessResponse { success: true; data: T; message?: string; } // 分页响应 interface PaginatedResponse { success: true; data: { items: T[]; pagination: { page: number; limit: number; total: number; totalPages: number; }; }; } // 错误响应 interface ErrorResponse { success: false; error: string; message: string; details?: any; } // 错误码定义 enum ErrorCode { VALIDATION_ERROR = 'VALIDATION_ERROR', UNAUTHORIZED = 'UNAUTHORIZED', FORBIDDEN = 'FORBIDDEN', NOT_FOUND = 'NOT_FOUND', RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', INTERNAL_ERROR = 'INTERNAL_ERROR', INVALID_TOKEN = 'INVALID_TOKEN', TOKEN_EXPIRED = 'TOKEN_EXPIRED', USER_NOT_FOUND = 'USER_NOT_FOUND', EMAIL_EXISTS = 'EMAIL_EXISTS', INVALID_CREDENTIALS = 'INVALID_CREDENTIALS', } ``` ### C. 版本历史 | 版本 | 日期 | 描述 | |-----|------|------| | 1.3.0 | 2026-02-04 | 更新提醒模块数据模型、重复提醒完成移除设置、逾期列表展开收起、无时间显示优化 | | 1.1.0 | 2026-01-29 | 添加SQLite本地开发支持 | | 1.0.0 | 2026-01-28 | 初始架构设计(PostgreSQL) | --- *文档维护: 系统架构组* *最后更新: 2026-02-04*