qia/docs/architecture.md
ddshi 025833c486 docs: 更新PRD和架构文档,记录v1.0.2版本功能变更
- 更新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>
2026-02-04 13:48:44 +08:00

62 KiB
Raw Permalink Blame History

纪念日管理系统 - 技术架构文档

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, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;');
  }
  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