- 更新PRD文档: - v1.0.2版本号和最近更新日期 - 重复提醒完成流程:移除repeat_type等重复设置 - 新增13.5逾期列表展开/收起功能 - 新增13.6无时间提醒显示优化 - 更新实施状态追踪和里程碑规划 - 更新架构文档: - reminders表结构更新为实际实现(type、repeat_type、repeat_interval等) - Prisma schema更新 - Zustand状态管理替代React Query - 前端项目结构更新 - 版本历史更新到v1.3.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
62 KiB
62 KiB
纪念日管理系统 - 技术架构文档
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 配置
主机: localhost
端口: 5432
数据库名: qia_db
用户: qia_user
连接池:
min: 2
max: 10
字符集: UTF8
时区: Asia/Shanghai
Redis 配置
主机: 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 表(用户)
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 表(纪念日)
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 表(提醒)
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 表(便签)
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对话历史)
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 表(密码重置)
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 完整实现
// 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)
// 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设计
// 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解析服务
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 动态获取与缓存策略
// 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配置
# /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进程管理配置
// 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常用命令:
# 启动应用
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证书自动续期
# /etc/cron.d/certbot-renew
# 每周一凌晨4点尝试续期
0 4 * * 1 root certbot renew --quiet && nginx -s reload
续期脚本:
#!/bin/bash
# /var/www/certbot/renew.sh
certbot renew --quiet
if [ $? -eq 0 ]; then
nginx -s reload
echo "证书已续期,Nginx已重载"
fi
8.4 systemd服务配置
# /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
# 启用服务
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认证中间件
// 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 请求限流
// 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 安全中间件
// 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 密码强度验证
// 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 代码分割与懒加载
// 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 缓存策略
// 使用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
// 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 数据库连接池
// 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缓存层
// 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 异步处理
// 使用消息队列处理非实时任务
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 定期维护脚本
-- 每月执行的维护脚本
-- 保存为 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 慢查询分析
-- 开启慢查询日志(需要重启数据库)
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.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响应格式规范
// 成功响应
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.3.0 | 2026-02-04 | 更新提醒模块数据模型、重复提醒完成移除设置、逾期列表展开收起、无时间显示优化 |
| 1.1.0 | 2026-01-29 | 添加SQLite本地开发支持 |
| 1.0.0 | 2026-01-28 | 初始架构设计(PostgreSQL) |
文档维护: 系统架构组 最后更新: 2026-02-04