2588 lines
62 KiB
Markdown
2588 lines
62 KiB
Markdown
# 纪念日管理系统 - 技术架构文档
|
||
|
||
## 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 | 日期处理 |
|
||
| React Query | 5.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,
|
||
anniversary_id UUID REFERENCES anniversaries(id) ON DELETE CASCADE,
|
||
title VARCHAR(200) NOT NULL,
|
||
remind_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||
is_completed BOOLEAN DEFAULT FALSE,
|
||
completed_at TIMESTAMP WITH TIME ZONE,
|
||
notification_type VARCHAR(20) DEFAULT 'email',
|
||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_reminders_user_id ON reminders(user_id);
|
||
CREATE INDEX idx_reminders_remind_time ON reminders(remind_time);
|
||
CREATE INDEX idx_reminders_is_completed ON reminders(is_completed);
|
||
CREATE INDEX idx_reminders_anniversary_id ON reminders(anniversary_id);
|
||
|
||
COMMENT ON COLUMN reminders.notification_type IS 'email:邮件, browser:浏览器推送';
|
||
```
|
||
|
||
#### 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")
|
||
anniversaryId String? @map("anniversary_id")
|
||
title String
|
||
remindTime DateTime @map("remind_time")
|
||
isCompleted Boolean @default(false) @map("is_completed")
|
||
completedAt DateTime? @map("completed_at")
|
||
notificationType String @default("email") @map("notification_type")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
anniversary Anniversary? @relation(fields: [anniversaryId], references: [id], onDelete: SetNull)
|
||
|
||
@@index([userId])
|
||
@@index([remindTime])
|
||
@@index([isCompleted])
|
||
@@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/
|
||
│ │ ├── common/
|
||
│ │ │ ├── Button/
|
||
│ │ │ ├── Modal/
|
||
│ │ │ ├── Input/
|
||
│ │ │ ├── Select/
|
||
│ │ │ ├── DatePicker/
|
||
│ │ │ ├── Toast/
|
||
│ │ │ └── Loading/
|
||
│ │ ├── layout/
|
||
│ │ │ ├── Header/
|
||
│ │ │ ├── Sidebar/
|
||
│ │ │ ├── Footer/
|
||
│ │ │ └── Layout/
|
||
│ │ ├── anniversary/
|
||
│ │ │ ├── AnniversaryCard/
|
||
│ │ │ ├── AnniversaryForm/
|
||
│ │ │ ├── AnniversaryList/
|
||
│ │ │ └── HolidayCard/
|
||
│ │ ├── reminder/
|
||
│ │ │ ├── ReminderItem/
|
||
│ │ │ ├── ReminderList/
|
||
│ │ │ └── ReminderBadge/
|
||
│ │ ├── note/
|
||
│ │ │ ├── NoteCard/
|
||
│ │ │ ├── NoteCanvas/
|
||
│ │ │ └── NoteEditor/
|
||
│ │ ├── ai/
|
||
│ │ │ ├── ChatBox/
|
||
│ │ │ ├── AIFloatingButton/
|
||
│ │ │ └── ParsingResult/
|
||
│ │ └── auth/
|
||
│ │ ├── LoginForm/
|
||
│ │ ├── RegisterForm/
|
||
│ │ ├── ForgotPasswordForm/
|
||
│ │ └── EmailVerification/
|
||
│ ├── 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/
|
||
│ │ ├── authStore.ts
|
||
│ │ ├── anniversaryStore.ts
|
||
│ │ ├── reminderStore.ts
|
||
│ │ ├── noteStore.ts
|
||
│ │ └── uiStore.ts
|
||
│ ├── 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<string> {
|
||
// 存入数据库
|
||
}
|
||
|
||
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<void> => {
|
||
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<void> => {
|
||
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, '"')
|
||
.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 (
|
||
<BrowserRouter>
|
||
<Suspense fallback={<LoadingSpinner fullScreen />}>
|
||
<Routes>
|
||
<Route path="/" element={<Home />} />
|
||
<Route path="/login" element={<Login />} />
|
||
<Route path="/register" element={<Register />} />
|
||
<Route path="/dashboard" element={<Dashboard />} />
|
||
<Route path="/anniversaries" element={<AnniversaryList />} />
|
||
<Route path="/notes" element={<NoteCanvas />} />
|
||
</Routes>
|
||
</Suspense>
|
||
</BrowserRouter>
|
||
);
|
||
}
|
||
|
||
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<typeof prismaClientSingleton>;
|
||
|
||
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<T>(key: string): Promise<T | null> {
|
||
const value = await redis.get(key);
|
||
return value ? JSON.parse(value) : null;
|
||
}
|
||
|
||
// 设置缓存
|
||
async set(key: string, value: any, ttlSeconds: number = 3600): Promise<void> {
|
||
await redis.setex(key, ttlSeconds, JSON.stringify(value));
|
||
}
|
||
|
||
// 删除缓存
|
||
async del(key: string): Promise<void> {
|
||
await redis.del(key);
|
||
}
|
||
|
||
// 批量删除
|
||
async delPattern(pattern: string): Promise<void> {
|
||
const keys = await redis.keys(pattern);
|
||
if (keys.length > 0) {
|
||
await redis.del(...keys);
|
||
}
|
||
}
|
||
|
||
// 检查键是否存在
|
||
async exists(key: string): Promise<boolean> {
|
||
return (await redis.exists(key)) === 1;
|
||
}
|
||
|
||
// 增量操作
|
||
async incr(key: string): Promise<number> {
|
||
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<T> {
|
||
success: true;
|
||
data: T;
|
||
message?: string;
|
||
}
|
||
|
||
// 分页响应
|
||
interface PaginatedResponse<T> {
|
||
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.1.0 | 2026-01-29 | 添加SQLite本地开发支持 |
|
||
| 1.0.0 | 2026-01-28 | 初始架构设计(PostgreSQL) |
|
||
|
||
---
|
||
|
||
*文档维护: 系统架构组*
|
||
*最后更新: 2026-01-29*
|