一个OJ系统的诞生(十)前端+部署

本文是一个OJ系统的诞生的最后一篇。前面9篇我们逐步剖析了整个后端,现在讲解的是前端和部署。前端是5个原生HTML页面+6个JS文件,不依赖任何前端框架;部署是start.sh+systemd一键启动。

1:本篇模块

cpp 复制代码
project-cpp-oj-vibecoding/
├── public/                      ← ★ 前端:所有用户看到的页面 ★
│   ├── index.html               ← 首页:题目列表
│   ├── login.html               ← 登录页
│   ├── register.html            ← 注册页
│   ├── problem.html             ← 题目详情 + 代码编辑器 + 提交
│   ├── admin.html               ← 管理后台
│   ├── css/
│   │   └── style.css            ← 全局样式(暗色主题)
│   └── js/
│       ├── api.js               ← fetch 封装,统一调用后端 API
│       ├── auth.js              ← 登录状态检查
│       ├── problem.js           ← 题目列表渲染
│       ├── problem_detail.js    ← 题目详情 + CodeMirror 编辑器
│       ├── submit.js            ← 提交 + 轮询结果
│       └── admin.js             ← 管理后台 CRUD 逻辑
├── scripts/
│   ├── start.sh                 ← 一键编译 + 启动
│   └── oj-backend.service       ← systemd 服务配置

前端技术选型

技术 为什么选它?
原生 HTML + CSS + JS 零依赖、零构建、打开即用。不需要 npm install、不需要 webpack、不需要 React/Vue。对于 5 个页面的小项目,原生够了
CodeMirror 5(CDN) 最流行的网页代码编辑器。CDN 引入,一行 HTML 就搞定。支持 C++ 语法高亮、行号、括号匹配、Tab 缩进
fetch API 浏览器原生支持的 HTTP 请求 API,不需要 axios/jQuery

2:api.js------前后端通信的电话线

所有前端页面都通过这个文件和后端通信。

cpp 复制代码
// ============================================================
// 文件名: api.js
// 作用: 封装 fetch API,统一处理所有后端请求
//
// 每个页面调用后端 API 时,都用这个函数:
//   api('/api/problems')          → GET 请求
//   api('/api/login', { body: JSON.stringify({...}) })  → POST 请求
//
// 它会自动:
//   1. 携带 Cookie(credentials: 'same-origin')
//   2. 设置 Content-Type 为 JSON
//   3. 解析返回的 JSON
//   4. 如果出错,抛出异常
// ============================================================

async function api(path, options = {}) {
    // 提取 headers(如果有的话),其他参数用 ...rest 接收
    const { headers, ...restOptions } = options;

    // 发送 HTTP 请求
    const resp = await fetch(path, {
        credentials: 'same-origin',  // ★ 关键:自动携带 Cookie
                                     // 这样后端才能获取 session_id
                                     // 没有这行 → 每次请求都是"未登录"状态
        ...restOptions,              // 其他参数(method, body 等)
        headers: {
            'Content-Type': 'application/json',  // 默认 JSON 格式
            ...headers,                           // 可覆盖
        },
    });

    // 解析响应文本
    const text = await resp.text();
    let data;

    // 尝试解析 JSON,失败就用文本作为错误信息
    try { data = JSON.parse(text); }
    catch (e) { data = { error: text || 'Unknown error' }; }

    // 如果 HTTP 状态码不是 2xx,抛出异常
    if (!resp.ok) throw new Error(data.error || `HTTP ${resp.status}`);
    // 这样调用者可以用 try-catch 统一处理错误

    return data;
}

使用事例

javascript 复制代码
// 在 login.html 中
async function handleLogin() {
    try {
        const data = await api('/api/login', {
            method: 'POST',
            body: JSON.stringify({
                username: 'alice',
                password: '123456'
            }),
        });
        alert('登录成功!欢迎 ' + data.username);
        window.location.href = '/';  // 跳转到首页
    } catch (e) {
        alert('登录失败:' + e.message);  // 比如 "Invalid username or password"
    }
}

3:auth.js------登陆状态管理

javascript 复制代码
// ==========================================================
// 文件名: auth.js
// 作用: 检查用户是否登录
//
// 每个需要登录才能访问的页面,都在页面加载时调用 requireAuth()
// 如果未登录,自动跳转到登录页
// ============================================================

// 检查登录状态:调用 /api/me 接口
async function checkAuth() {
    try {
        const data = await api('/api/me');   // 获取当前用户信息
        return { ok: true, user: data };      // 已登录
    } catch (e) {
        return { ok: false, error: e.message };  // 未登录或过期
    }
}

// 强制要求登录:如果未登录,跳转到登录页
async function requireAuth(redirect = '/login.html') {
    const result = await checkAuth();
    if (!result.ok) {
        window.location.href = redirect;  // 跳转到登录页
        return null;
    }
    return result.user;  // 返回用户信息
}

在页面中的使用

html 复制代码
<!-- problem.html 页面加载时 -->
<script src="/js/api.js"></script>
<script src="/js/auth.js"></script>
<script>
    // 页面加载时立即检查登录
    // 如果未登录,跳转到 login.html
    requireAuth();

    // 或者可以拿到用户信息
    requireAuth().then(user => {
        console.log('当前用户:', user.username);
        console.log('角色:', user.role);
    });
</script>

Cookie鉴权的完整流程

html 复制代码
浏览器                   服务器
  │                       │
  │  POST /api/login      │
  │  {"username":"alice"} │
  │──────────────────────→│
  │                       │ 验证密码 
  │                       │ 生成 session_id
  │                       │ 写 Session 文件
  │  200 OK               │
  │  Set-Cookie:          │
  │  session_id=a1b2...   │
  │←──────────────────────│
  │                       │
  │  ★ 浏览器自动保存 Cookie
  │                       │
  │  GET /api/me          │
  │  Cookie: session_id=  │
  │  a1b2...              │
  │──────────────────────→│
  │                       │ 读 Session 文件
  │                       │ 返回用户信息
  │  200 OK               │
  │  {"id":1,"username":  │
  │   "alice"}            │
  │←──────────────────────│

4:5个页面职责

1:index.html------题目列表

html 复制代码
┌─────────────────────────────────────────────────┐
│  标题: OJ System                                 │
│  导航: [首页] [管理后台] [登出]                    │
├─────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────┐│
│  │ 题目列表                                     ││
│  ├──────┬──────────┬──────────┬──────────┬────┤│
│  │  ID  │  标题     │  难度    │ 通过率   │ 提交││
│  ├──────┼──────────┼──────────┼──────────┼────┤│
│  │  1   │ 两数相加   │ easy     │ 65.5%    │ 200││
│  │  2   │ 反转链表   │ medium   │ 80.0%    │ 50 ││
│  │  3   │ 红黑树    │ hard     │ 10.0%    │ 100││
│  └──────┴──────────┴──────────┴──────────┴────┘│
└─────────────────────────────────────────────────┘

核心JS逻辑

javascript 复制代码
// problem.js --- 加载并渲染题目列表
async function loadProblems() {
    const data = await api('/api/problems');  // GET /api/problems
    const table = document.getElementById('problem-table');

    for (const p of data) {
        const row = table.insertRow();
        row.innerHTML = `
            <td>${p.id}</td>
            <td><a href="/problem.html?id=${p.id}">${p.title}</a></td>
            <td><span class="badge-${p.difficulty}">${p.difficulty}</span></td>
            <td>${p.pass_rate.toFixed(1)}%</td>
            <td>${p.total_submissions}</td>
        `;
    }
}

// 页面加载时执行
loadProblems();

2:login.html------登陆页面

·

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>登录 - OJ System</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <div class="form-container">
        <h2>登录</h2>
        <form id="loginForm">
            <input type="text" id="username" placeholder="用户名" required>
            <input type="password" id="password" placeholder="密码" required>
            <button type="submit">登录</button>
        </form>
        <p>还没有账号?<a href="/register.html">注册</a></p>
    </div>

    <script src="/js/api.js"></script>
    <script>
        document.getElementById('loginForm').onsubmit = async (e) => {
            e.preventDefault();

            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;

            try {
                const data = await api('/api/login', {
                    method: 'POST',
                    body: JSON.stringify({ username, password }),
                });
                // ★ 登录成功!服务器设置了 Set-Cookie
                // 浏览器自动保存了 Cookie
                // 下次请求自动携带
                alert('登录成功!');
                window.location.href = '/';  // 跳转到首页
            } catch (e) {
                alert('登录失败:' + e.message);
            }
        };
    </script>
</body>
</html>

3:register.html------注册页面

html 复制代码
<!-- 和登录页面非常相似,只是调用 POST /api/register -->
<script>
    document.getElementById('registerForm').onsubmit = async (e) => {
        e.preventDefault();
        try {
            const data = await api('/api/register', {
                method: 'POST',
                body: JSON.stringify({
                    username: document.getElementById('username').value,
                    password: document.getElementById('password').value,
                }),
            });
            alert('注册成功!请登录');
            window.location.href = '/login.html';
        } catch (e) {
            alert('注册失败:' + e.message);
        }
    };
</script>

4:problem.html------题目详细+编辑器+提交

这是最核心的前端页面,包含了:题目展示、代码编辑器、提交按钮、结果展示。

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>题目详情 - OJ System</title>
    <!-- CodeMirror 5(CDN 引入,不需要安装) -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/codemirror.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/codemirror.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/mode/clike/clike.min.js"></script>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <div id="problem-container">
        <!-- 左侧:题目描述 -->
        <div id="problem-desc">
            <h2 id="title"></h2>
            <div id="description"></div>
            <h3>输入格式</h3>
            <div id="input-desc"></div>
            <h3>输出格式</h3>
            <div id="output-desc"></div>
            <h3>示例</h3>
            <div id="samples"></div>
        </div>

        <!-- 右侧:代码编辑器 + 提交 -->
        <div id="editor-panel">
            <textarea id="code-editor"></textarea>
            <button id="submit-btn">提交</button>
            <div id="result"></div>
        </div>
    </div>

    <script src="/js/api.js"></script>
    <script src="/js/auth.js"></script>
    <script src="/js/problem_detail.js"></script>
    <script src="/js/submit.js"></script>
</body>
</html>

problem_detail.js ------ 加载题目详情 + 初始化编辑器

javascript 复制代码
// ============================================================
// 文件名: submit.js
// 作用: 提交代码 + 轮询判题结果
//
// 这是"异步判题"的前端实现:
//   1. 提交代码 → 后端返回 202 + submission_id
//   2. 开始轮询:每隔 1 秒查一次结果
//   3. 等到状态变成 AC/WA/CE 等最终状态
//   4. 展示结果
// ============================================================

document.getElementById('submit-btn').onclick = async () => {
    const code = editor.getValue();  // 从 CodeMirror 获取代码

    if (!code.trim()) {
        alert('请先写代码!');
        return;
    }

    const resultDiv = document.getElementById('result');
    resultDiv.innerHTML = '提交中...';
    document.getElementById('submit-btn').disabled = true;

    try {
        // 第 1 步:提交代码
        const submitResult = await api('/api/submit', {
            method: 'POST',
            body: JSON.stringify({
                problem_id: parseInt(problemId),
                code: code,
            }),
        });

        const submissionId = submitResult.submission_id;
        resultDiv.innerHTML = '判题中... (PENDING)';

        // 第 2 步:轮询结果
        // 每隔 1 秒查询一次,直到状态不再是 PENDING/JUDGING
        const poll = setInterval(async () => {
            try {
                const result = await api(`/api/submissions/${submissionId}`);

                // 更新状态显示
                resultDiv.innerHTML = `判题中... (${result.status})`;

                // 如果到了最终状态,停止轮询并显示结果
                if (!['PENDING', 'JUDGING'].includes(result.status)) {
                    clearInterval(poll);
                    document.getElementById('submit-btn').disabled = false;

                    // 根据状态显示不同的结果
                    if (result.status === 'AC') {
                        resultDiv.innerHTML = `
                            <div class="result-ac">✅ 通过 (Accepted)</div>
                            <div>用时: ${result.time_used}ms</div>
                            <div>内存: ${result.memory_used}KB</div>
                        `;
                    } else if (result.status === 'WA') {
                        resultDiv.innerHTML = `
                            <div class="result-wa">❌ 答案错误 (Wrong Answer)</div>
                            <div>失败用例: #${result.failed_case}</div>
                            <div>你的输出: ${result.failed_actual || '?'}</div>
                            <div>期望输出: ${result.failed_expected || '?'}</div>
                            <div>输入: ${result.failed_input || '?'}</div>
                        `;
                    } else if (result.status === 'CE') {
                        resultDiv.innerHTML = `
                            <div class="result-ce">⚠️ 编译错误 (Compilation Error)</div>
                            <pre>${result.error_msg}</pre>
                        `;
                    } else if (result.status === 'TLE') {
                        resultDiv.innerHTML = `
                            <div class="result-tle">⏱️ 超时 (Time Limit Exceeded)</div>
                            <div>用时: ${result.time_used}ms</div>
                        `;
                    } else {
                        resultDiv.innerHTML = `
                            <div>状态: ${result.status}</div>
                            <div>${result.error_msg || ''}</div>
                        `;
                    }
                }
            } catch (e) {
                clearInterval(poll);
                document.getElementById('submit-btn').disabled = false;
                resultDiv.innerHTML = `查询失败: ${e.message}`;
            }
        }, 1000);  // 1 秒轮询一次

    } catch (e) {
        document.getElementById('submit-btn').disabled = false;
        resultDiv.innerHTML = `提交失败: ${e.message}`;
    }
};

轮询流程图

javascript 复制代码
用户点击"提交"
    │
    ▼
POST /api/submit → 202 Accepted + submission_id
    │
    ▼
setInterval 开始轮询(每 1 秒)
    │
    ├── GET /api/submissions/42 → { status: "PENDING" }
    │   └── 显示 "判题中... (PENDING)"
    │
    ├── GET /api/submissions/42 → { status: "JUDGING" }
    │   └── 显示 "判题中... (JUDGING)"
    │
    ├── GET /api/submissions/42 → { status: "AC", time_used: 15, ... }
    │   └── 显示  "通过!用时 15ms,内存 2048KB"
    │   └── clearInterval → 停止轮询
    │
    └──(如果一直是 PENDING/JUDGING,继续轮询)

5:admin.html------管理后台

管理员可以增删改题目和管理测试用例。主要功能:

javascript 复制代码
┌─────────────────────────────────────────────────┐
│  管理后台                                        │
├─────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────┐│
│  │ 题目列表 + [新增题目] 按钮                    ││
│  ├──────┬──────────┬──────────┬────────────────┤│
│  │  ID  │  标题     │  难度    │  操作           ││
│  ├──────┼──────────┼──────────┼────────────────┤│
│  │  1   │ 两数相加   │ easy     │ [编辑] [删除]   ││
│  │  2   │ 反转链表   │ medium   │ [编辑] [删除]   ││
│  └──────┴──────────┴──────────┴────────────────┘│
│                                                  │
│  ┌─────────────────────────────────────────────┐│
│  │ 测试用例管理(点击某道题的"编辑"后弹出)      ││
│  │ 可以查看/添加/删除测试用例                    ││
│  │ 每个用例可以设置:is_sample 是否展示给用户    ││
│  └─────────────────────────────────────────────┘│
└─────────────────────────────────────────────────┘

5:style.css------暗色主题

css 复制代码
/* 全局设置 */
body {
    font-family: 'Segoe UI', sans-serif;
    background-color: #1a1a2e;       /* 深色背景 */
    color: #e0e0e0;                  /* 浅色文字 */
    margin: 0;
    padding: 20px;
}

/* 表单容器(登录/注册页) */
.form-container {
    max-width: 400px;
    margin: 100px auto;
    padding: 30px;
    background: #16213e;             /* 稍亮的深色卡片 */
    border-radius: 8px;
}

/* 输入框 */
input[type="text"],
input[type="password"],
textarea {
    width: 100%;
    padding: 10px;
    margin: 8px 0;
    background: #0f3460;
    border: 1px solid #333;
    color: #fff;
    border-radius: 4px;
}

/* 按钮 */
button {
    background: #e94560;             /* 红色按钮 */
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 4px;
    cursor: pointer;
}

button:hover {
    background: #c73e54;
}

/* 难度标签 */
.badge-easy { color: #4caf50; }     /* 绿色 */
.badge-medium { color: #ff9800; }   /* 橙色 */
.badge-hard { color: #f44336; }     /* 红色 */

/* 判题结果 */
.result-ac { color: #4caf50; font-size: 1.2em; }   /* 绿色通过 */
.result-wa { color: #f44336; font-size: 1.2em; }   /* 红色错误 */
.result-ce { color: #ff9800; font-size: 1.2em; }   /* 橙色编译错 */
.result-tle { color: #9c27b0; font-size: 1.2em; }  /* 紫色超时 */

6:start.sh------布置脚本

bash 复制代码
#!/bin/bash
set -e  # 任何命令失败就退出(不继续执行)

# 获取项目根目录路径
# "$(dirname "$0")" 是脚本所在目录(scripts/)
# "/.." 回到项目根目录
PROJ="$(cd "$(dirname "$0")/.." && pwd)"
BUILD_DIR="$PROJ/build"

echo "==> Creating runtime directories..."
# 创建 Session 目录和沙箱目录
sudo mkdir -p /var/oj/sessions /tmp/oj_sandbox
# 把目录所有权给当前用户(这样不需要 root 也能写文件)
sudo chown -R "$(whoami)" /var/oj/sessions /tmp/oj_sandbox 2>/dev/null || true

echo "==> Building..."
# 在 build/ 目录中生成 CMake 构建文件
cmake -S "$PROJ" -B "$BUILD_DIR"
# 编译 oj_backend 可执行文件
cmake --build "$BUILD_DIR" --target oj_backend -j$(nproc)

echo "==> Starting oj_backend..."
cd "$PROJ"
# exec 替换当前 shell 进程为 oj_backend
# 这样 Ctrl+C 直接发给 oj_backend,而不是 shell
exec "$BUILD_DIR/oj_backend" "$PROJ/config/config.json"

使用方法

一键编译 + 启动

bash scripts/start.sh

如果不想编译,直接运行已有的可执行文件

./build/oj_backend config/config.json

7:systemd服务------开机自启

bash 复制代码
# scripts/oj-backend.service
# 这是一个 systemd 服务配置文件
# systemd 是 Linux 系统的"进程管理器"------负责启动、停止、监控服务
#
# 安装方式:
#   sudo cp scripts/oj-backend.service /etc/systemd/system/
#   sudo systemctl daemon-reload
#   sudo systemctl enable --now oj-backend
#   (enable 是开机自启,--now 是立即启动)

[Unit]
Description=OJ Backend Service          # 服务描述
After=network.target mysql.service      # 在网络和 MySQL 之后启动
                                        # 确保 MySQL 已经运行了

[Service]
Type=simple                             # 简单类型:主进程就是服务
User=oj                                  # 以 oj 用户运行(安全和权限考虑)
WorkingDirectory=/opt/oj                # 工作目录
ExecStart=/opt/oj/build/oj_backend /opt/oj/config/config.json
                                        # 启动命令
Restart=on-failure                      # 失败时自动重启
RestartSec=5                            # 重启前等待 5 秒

[Install]
WantedBy=multi-user.target              # 多用户模式下启动(正常启动)

8:从开发到部署的完整流程

1:开发环境

bash 复制代码
# 1. 安装依赖
sudo apt install cmake g++ libmysqlclient-dev libseccomp-dev libssl-dev mysql-server

# 2. 初始化数据库
sudo mysql < database/init.sql

# 3. 配置 config.json(改数据库密码)
vim config/config.json

# 4. 编译 + 启动
bash scripts/start.sh

# 5. 打开浏览器访问
# http://localhost:8080

2:环境部署

bash 复制代码
# 1. 把项目传到服务器
git clone git@github.com:yourname/project-cpp-oj-vibecoding.git
cd project-cpp-oj-vibecoding

# 2. 安装依赖
sudo apt install cmake g++ libmysqlclient-dev libseccomp-dev libssl-dev mysql-server

# 3. 初始化数据库
sudo mysql < database/init.sql

# 4. 配置 config.json
vim config/config.json  # 修改数据库密码等

# 5. 安装 systemd 服务
sudo cp scripts/oj-backend.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now oj-backend

# 6. 查看运行状态
sudo systemctl status oj-backend

9:项目完整回顾

bash 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                      一个 OJ 系统的诞生                             │
│                        12 篇博客                                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  [01] 项目动机 + 技术选型 + AI 辅助设计                              │
│       为什么做?为什么用 C++?4 层架构长什么样?                       │
│                                                                     │
│  [02] 工具层 --- Config + Logger                                      │
│       单例模式、JSON 配置、日志级别、RAII 锁                          │
│       文件: src/utils/config.hpp/cc + logger.hpp/cc                  │
│                                                                     │
│  [03] 数据库层 --- MySQL 连接池                                        │
│       vector 存连接、mutex 保护、condition_variable 等待/通知        │
│       文件: src/db/connection_pool.hpp/cc                            │
│                                                                     │
│  [04] 模型层 --- 数据快递盒                                            │
│       3 个 struct:User / Problem / TestCase(47 行代码)            │
│       文件: src/model/user.hpp / problem.hpp / test_case.hpp         │
│                                                                     │
│  [05] Service 层上 --- AuthService                                   │
│       注册/登录/bcrypt 密码哈希/Session 文件/限流/后台清理            │
│       文件: src/service/auth_service.cc(414 行)                    │
│                                                                     │
│  [06] Service 层中 --- ProblemService                                │
│       LEFT JOIN 多表查询、封装 escape() 防 SQL 注入、级联删除         │
│       文件: src/service/problem_service.cc(291 行)                  │
│                                                                     │
│  [07] Service 层下 --- ExecutorService                            │
│       任务队列 + Worker 线程、fork 子进程、seccomp 沙箱、            │
│       setrlimit 资源限制、编译运行比对                               │
│       文件: src/service/executor_service.cc(506 行)                │
│                                                                     │
│  [08] Handler 层 --- HTTP 接线员                                      │
│       15 个 API、12 种 HTTP 状态码、Cookie 设置/清除、6 道安检门      │
│       文件: src/handler/ 下的 4 个文件                               │
│                                                                     │
│  [09] main.cc --- 总指挥                                              │
│       初始化顺序、信号处理优雅关闭、CMake 构建                         │
│       文件: src/main.cc + src/server/                               │
│                                                                     │
│  [10] 前端 + 部署 --- 最后一公里                                      │
│       原生 HTML/JS/CSS、CodeMirror 编辑器、fetch API 轮询、          │
│       start.sh + systemd 一键部署                                    │
│       文件: public/ + scripts/                                       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

数据量统计

维度 数字
C++ 后端 约 1800 行(不含第三方库)
前端 5 个 HTML 页面、6 个 JS 文件、1 个 CSS
数据库 4 张表
API 15 个 REST 接口
判题状态 7 种(PENDING → JUDGING → AC/WA/CE/RE/TLE/MLE)
安全防线 5 层(seccomp /setrlimit/prctl /pipe/ 超时监控)
代码总行数 约 3000 行(含前端)

10:结束语

从一个模糊的想法------"我想做一个 OJ 系统",经过 10 篇博客的逐层解剖,我们看到了一个完整的在线判题系统是如何从零到一构建起来的。

这个项目不大,但五脏俱全:

它有**完整的用户系统**(注册、登录、Session、限流)

它有**完整的题目系统**(增删改查、测试用例管理)

它有**完整的判题引擎**(编译、沙箱、逐用例运行、结果比对)

它有**完整的前端界面**(5 个页面、代码编辑器、轮询)

它有**完整的部署方案**(编译脚本、systemd 服务)

除了代码本身,更重要的是代码背后的设计思想

**单例模式**------全局唯一的配置、日志、连接池

**分层架构**------Router → Handler → Service → Model → DB,各司其职

**防御性编程**------处处有默认值、处处有错误处理、处处有安全检查

**最小可行产品**------不追求完美,追求够用。文件 Session 而不是 Redis,原生 JS 而不是 React,够用就好