# Mini OJ — 项目详解文档

> 从零构建一个仿 LeetCode 在线判题平台:C++17 后端 + 原生前端 + MySQL

> 不需要 Docker、不需要 Nginx、不需要任何第三方 C++ 库

--

目录

  1. 项目概述(#1-项目概述)

  2. 系统架构(#2-系统架构)

  3. 前端实现详解(#3-前端实现详解)

  4. 后端实现详解(#4-后端实现详解)

  5. 安全性设计(#5-安全性设计)

  6. 部署运维(#6-部署运维)

  7. 踩坑记录(#7-踩坑记录)


1. 项目概述

1.1 这是什么?

Mini OJ 是一个轻量级的**在线判题系统**(Online Judge),核心功能是:用户提交 C++ 代码 → 服务端编译运行 → 与预期输出比对 → 返回判题结果。

1.2 技术栈一览

| 层 | 技术 | 为什么选它 |

|---|------|-----------|

| 后端 | C++17 + cpp-httplib | 高性能,header-only 零依赖 |

| 数据库 | MySQL 8.0 | 关系型,适合题目/提交数据 |

| 前端 | 原生 HTML/CSS/JS | 零构建工具,部署即静态文件 |

| CSS | Water.css | classless 自动暗色模式 |

| 安全 | fork + setrlimit | 进程级隔离,无需 Docker |

1.3 核心功能

  • **用户系统**:注册/登录/登出,Session + Bearer Token 认证

  • **题目浏览**:题目列表 + 详情页,支持 Markdown 描述、样例展示

  • **在线提交**:编辑器提交 C++ 代码,实时轮询判题结果

  • **判题引擎**:编译 → 沙箱运行 → 逐用例比对 → 6 种状态反馈

  • **管理后台**:CRUD 题目 + 管理测试用例

  • **提交记录**:查看历史提交、每个用例的详细结果


2. 系统架构

2.1 整体拓扑

```

┌─────────────────────────────────────────────────┐

│ 浏览器 │

│ ┌──────────────────────────────────────────┐ │

│ │ SPA (Hash Router) │ │

│ │ 首页 → 登录 → 题目列表 → 详情+提交 → 结果 │ │

│ └──────────────────────────────────────────┘ │

└──────────────────┬──────────────────────────────┘

│ HTTP (8080)

┌──────────────────▼──────────────────────────────┐

│ cpp-httplib HTTP Server │

│ ┌─────────────┐ ┌────────────────────────────┐ │

│ │ 静态文件服务 │ │ 12 个 API Handler │ │

│ │ (frontend/) │ │ /api/auth/* │ │

│ │ │ │ /api/problems/* │ │

│ │ │ │ /api/submissions/* │ │

│ │ │ │ /api/admin/* │ │

│ └─────────────┘ └──────────┬─────────────────┘ │

│ │ │

│ ┌───────────────────────────▼─────────────────┐ │

│ │ Judge Manager (2 线程) │ │

│ │ ┌──────────┐ ┌──────────────────────┐ │ │

│ │ │ 编译 g++ │→│ fork + setrlimit 沙箱 │ │ │

│ │ └──────────┘ └──────────────────────┘ │ │

│ └────────────────────────────────────────────┘ │

└──────────────────┬──────────────────────────────┘

│ TCP (3306)

┌──────────────────▼──────────────────────────────┐

│ MySQL 8.0 │

│ users | problems | test_cases | submissions │

└─────────────────────────────────────────────────┘

```

2.2 目录结构

```

├── database/init.sql # 建库建表 + 示例 A+B Problem

├── backend/

│ ├── CMakeLists.txt # 构建配置

│ ├── src/

│ │ ├── main.cpp # 入口:init() → start()

│ │ ├── config.h # 所有可配置常量

│ │ ├── server.h # HTTP 路由 + 12 个 Handler

│ │ ├── auth.h # Session 管理器(内存)

│ │ ├── db.h # MySQL RAII 连接池

│ │ ├── utils.h # JSON 构建 / SHA-256 / 密码哈希

│ │ └── judge_manager.h # 判题引擎(编译+沙箱+队列)

│ └── third_party/

│ └── cpp-httplib.h # ← 需手动下载

├── frontend/

│ ├── index.html # SPA 入口

│ ├── css/app.css # 自定义样式

│ └── js/

│ ├── app.js # 路由注册 + 启动

│ ├── router.js # Hash Router

│ ├── api.js # API 请求封装

│ ├── auth.js # 登录状态管理

│ ├── components/ # navbar / problem-table / judge-result

│ └── pages/ # 7 个页面(含首页)

├── SPEC.md # 完整规格文档

├── DEPLOY.md # 部署指南

└── README.md # 快速开始

```


3. 前端实现详解

3.1 Hash Router --- 零依赖 SPA 路由

前端采用 **Hash-based SPA** 架构,全部路由逻辑不到 120 行。

**核心匹配算法:**

```javascript

// router.js --- 路由匹配核心

function match(pattern, path) {

const patternParts = pattern.split('/');

const pathParts = path.split('/');

if (patternParts.length !== pathParts.length) return null;

const params = {};

for (let i = 0; i < patternParts.length; i++) {

if (patternPartsi.startsWith(':')) {

// 动态参数:/problem/:id 匹配 /problem/5 → {id: "5"}

paramspatternParts\[i.slice(1)] = pathPartsi;

} else if (patternPartsi !== pathPartsi) {

return null;

}

}

return params;

}

```

**设计亮点:**

  • 模式匹配支持 `:param` 动态参数,如 `/problem/:id` 可匹配 `/problem/1`、`/problem/99`

  • `queryParams()` 独立解析 `?key=value` 查询字符串,不与路由耦合

  • 默认路由 `home` --- 未登录展示介绍页,已登录自动跳转题目列表

**注册路由(app.js):**

```javascript

Router.register('home', (p) => Pages.Landing.render(p));

Router.register('auth', (p) => Pages.Landing.render(p));

Router.register('problems', (p) => Pages.ProblemList.render(p));

Router.register('problem/:id', (p) => Pages.ProblemDetail.render(p));

Router.register('submissions', (p) => Pages.SubmissionHistory.render(p));

Router.register('admin', (p) => Pages.AdminProblemList.render(p));

Router.register('admin/problem/:id', (p) => Pages.AdminProblemEdit.render(p));

```

> ⚠️ **踩坑**:传递方法引用如 `Pages.ProblemDetail.render` 会丢失 `this` 上下文,必须用箭头函数 `(p) => Pages.ProblemDetail.render(p)` 包裹。

3.2 判题结果轮询

提交代码后,前端每秒轮询一次结果,直到判题完成:

```javascript

// problem-detail.js --- 轮询判题结果

_pollForResult(submissionId) {

let attempts = 0;

this._pollTimer = setInterval(async () => {

const result = await API.getSubmission(submissionId);

attempts++;

if (result.status !== 'pending' &&

result.status !== 'compiling' &&

result.status !== 'running') {

// 判题完成,渲染结果

clearInterval(this._pollTimer);

document.getElementById('judgeResultPanel').innerHTML =

JudgeResult.render(result);

return;

}

// 60 秒超时保护

if (attempts > 60) {

clearInterval(this._pollTimer);

// 提示用户手动刷新

}

}, 1000);

}

```

**设计亮点**:不是 WebSocket 而是轮询 --- 足够简单,60 行能搞定的事不需要引入额外协议。

3.3 组件化设计

```javascript

// judge-result.js --- 可复用判题结果组件

const JudgeResult = {

render(result) {

// 汇总信息:AC/WA/TLE + 总用时 + 总内存

// 编译错误展示(如 CE)

// 逐用例详情表格:用例# / 结果 / 用时 / 内存 / 期望 / 实际

}

};

// problem-table.js --- 可复用题目表格

const ProblemTable = {

render(problems, { isAdmin, onDelete } = {}) {

// 普通用户:查看题目

// 管理员:额外显示 编辑/删除 操作按钮

}

};

```

**设计理念**:组件只负责渲染,不处理路由跳转。解耦后任何页面都能复用同一套组件。

3.4 首页设计

未登录用户看到的首页:

```

⚡ Mini OJ(带脉冲动画)

轻量级在线判题平台

┌──────────┐ ┌──────────┐ ┌──────────┐

│ 🚀 在线判题│ │ 🛡️ 安全沙箱│ │ 📚 题目管理│

└──────────┘ └──────────┘ └──────────┘

┌─ 登录 ─┬─ 注册 ─┐

│ 登录表单 │

└──────────────────┘

```

登录后自动跳转题目列表,导航栏展开完整功能。


4. 后端实现详解

4.1 判题引擎 --- 进程沙箱隔离

这是整个项目最核心、最精巧的部分。

4.1.1 三步流水线

```

用户提交代码

┌──────────┐

│ 1. 编译 │ g++ -O2 -std=c++17 -Wall → 二进制文件

└────┬─────┘

│ 成功

┌──────────┐

│ 2. 运行 │ fork() → setrlimit() → alarm() → execl()

└────┬─────┘

┌──────────┐

│ 3. 比对 │ trim(实际输出) == trim(期望输出) ?

└──────────┘

```

4.1.2 编译阶段

```cpp

// judge_manager.h --- 编译

bool compile(const std::string& code_path, const std::string& bin_path,

std::string& error) {

std::string cmd = "g++ -O2 -std=c++17 -Wall -o "

  • bin_path + " " + code_path + " 2>&1";

std::array<char, 256> buffer;

FILE* pipe = popen(cmd.c_str(), "r"); // 捕获 g++ 输出

while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {

error += buffer.data(); // 收集编译错误信息

}

return (pclose(pipe) == 0); // 返回码 0 = 编译成功

}

```

4.1.3 沙箱运行 --- 核心安全机制

```cpp

// judge_manager.h --- run_test() 核心流程

pid_t pid = fork();

if (pid == 0) {

// ========== 子进程:用户代码运行环境 ==========

// 1) CPU 时间限制:超限 → SIGXCPU

struct rlimit rl;

rl.rlim_cur = cpu_sec;

rl.rlim_max = cpu_sec + 1;

setrlimit(RLIMIT_CPU, &rl);

// 2) 内存限制:超限 → malloc 失败 / SIGKILL

rl.rlim_cur = memory_limit_kb * 1024;

rl.rlim_max = memory_limit_kb * 1024 + 1024 * 1024;

setrlimit(RLIMIT_AS, &rl);

// 3) 进程数限制 = 1:防止 fork 炸弹

rl.rlim_cur = 1;

rl.rlim_max = 1;

setrlimit(RLIMIT_NPROC, &rl);

// 4) I/O 重定向:stdin ← 输入文件, stdout → 输出文件, stderr → /dev/null

dup2(fd_in, STDIN_FILENO);

dup2(fd_out, STDOUT_FILENO);

dup2(fd_null, STDERR_FILENO);

// 5) 执行编译好的二进制

execl(bin_path.c_str(), bin_path.c_str(), nullptr);

_exit(127); // execl 失败才到这里

}

// ========== 父进程:监控与计时 ==========

// 设置 SIGALRM 处理:只打断 wait4(),不杀死进程

struct sigaction sa;

sa.sa_handler = alarm_noop; // 空函数

sa.sa_flags = 0; // 不设置 SA_RESTART

sigaction(SIGALRM, &sa, &old_sa);

alarm(wall_sec); // 挂钟超时 = 用户时限 + 2 秒缓冲

pid_t waited = wait4(pid, &status, 0, &rusage); // 阻塞等待

alarm(0); // 取消闹钟

sigaction(SIGALRM, &old_sa, nullptr); // 恢复旧 handler

```

**三层防护机制:**

| 限制 | 机制 | 触发条件 | 判题结果 |

|------|------|---------|---------|

| CPU 时间 | `setrlimit(RLIMIT_CPU)` | 用户代码循环/死循环 | TLE (SIGXCPU) |

| 内存 | `setrlimit(RLIMIT_AS)` | 数组过大/内存泄漏 | MLE (SIGKILL) |

| 挂钟时间 | `alarm(wall_sec)` | I/O 阻塞/恶意 sleep | TLE (SIGALRM) |

> **为什么需要三层?** CPU 限制挡不住 `sleep(100)` 这种不耗 CPU 的恶意代码;内存限制挡不住死循环占着 CPU 不放。必须配合使用。

4.1.4 信号处理 --- 踩坑与修复

**最初的 Bug**:`alarm()` 不设置 handler,SIGALRM 直接杀死**整个后端进程**,导致任何人提交超时代码都能让服务崩溃。

**修复方式**:安装一个空 handler,且**不设置 SA_RESTART**:

```cpp

static void alarm_noop(int) {

// 空函数 --- 什么也不做,但 prevent 进程被杀

}

struct sigaction sa;

sa.sa_handler = alarm_noop;

sa.sa_flags = 0; // ⚠️ 关键:不设 SA_RESTART → wait4() 收到 EINTR 返回

```

**原理**:`sigaction` 默认行为是收到信号后自动重启被中断的系统调用。但这里我们需要 SIGALRM **打断** `wait4()`,所以显式不设置 `SA_RESTART`。`wait4()` 被中断后返回 -1(errno=EINTR),父进程就知道超时了。

4.1.5 结果判定

```cpp

// judge_manager.h --- 信号 → 状态映射

if (WIFEXITED(status)) {

int exit_code = WEXITSTATUS(status);

if (exit_code != 0) {

result.status = "runtime_error"; // 任何非零退出码 → RE

}

} else if (WIFSIGNALED(status)) {

switch (WTERMSIG(status)) {

case SIGALRM: // alarm 超时

case SIGXCPU: // RLIMIT_CPU 超限

result.status = "time_limit_exceeded";

break;

case SIGSEGV: // 段错误

case SIGABRT: // abort()

case SIGFPE: // 除零/浮点异常

result.status = "runtime_error";

break;

case SIGKILL: // 通常是被 OOM Killer 或 RLIMIT_AS 杀

result.status = "memory_limit_exceeded";

break;

}

}

// 然后再比对输出

if (result.status == "accepted") {

if (trim(actual_output) != trim(expected_output)) {

result.status = "wrong_answer";

}

}

```

**资源统计**:`wait4()` 返回的 `rusage` 结构体提供精确的用户态/内核态 CPU 时间和最大常驻内存:

```cpp

result.time_ms = rusage.ru_utime.tv_sec * 1000 + rusage.ru_utime.tv_usec / 1000

  • rusage.ru_stime.tv_sec * 1000 + rusage.ru_stime.tv_usec / 1000;

result.memory_kb = rusage.ru_maxrss;

```

4.2 生产者-消费者判题队列

```cpp

// judge_manager.h --- 队列模型

void enqueue(int submission_id, int problem_id, const std::string& code) {

JudgeTask task{submission_id, problem_id, code};

{

std::lock_guard<std::mutex> lock(queue_mutex_);

task_queue_.push(task);

}

cv_.notify_one(); // 唤醒一个等待的工作线程

}

void worker_loop() {

while (running_) {

JudgeTask task;

{

std::unique_lock<std::mutex> lock(queue_mutex_);

cv_.wait(lock, this {

return !task_queue_.empty() || !running_;

});

if (!running_) return;

task = task_queue_.front();

task_queue_.pop();

}

judge_task(task); // 编译 → 运行 → 比对 → 写库

}

}

```

**设计要点**:

  • 使用 `condition_variable` 而非忙等 --- 空闲时线程休眠,不耗 CPU

  • `cv_.wait()` 的谓词检查 `!running_` --- 优雅关闭时能立即退出

  • 默认 2 个工作线程(`config::MAX_CONCURRENT_JUDGE`),支持并发判题

4.3 纯 C++ SHA-256 实现

**最初的实现**(严重安全漏洞):

```cpp

// ❌ 危险:命令注入!

std::string cmd = "echo -n '" + input + "' | sha256sum";

FILE* pipe = popen(cmd.c_str(), "r");

// 输入 abc'; rm -rf /; echo ' → 灾难

```

**最终实现** --- 完整 FIPS 180-4 算法:

```cpp

// utils.h --- SHA256 类

class SHA256 {

void process_block(const uint8_t* block) {

// 1. 消息调度:16 个 32-bit 字 → 64 个

uint32_t w64;

for (int i = 0; i < 16; ++i)

wi = (blocki\*4<<24) | (blocki\*4+1<<16)

| (blocki\*4+2<<8) | blocki\*4+3;

for (int i = 16; i < 64; ++i) {

uint32_t s0 = rotr(wi-15,7) ^ rotr(wi-15,18) ^ (wi-15>>3);

uint32_t s1 = rotr(wi-2,17) ^ rotr(wi-2,19) ^ (wi-2>>10);

wi = wi-16 + s0 + wi-7 + s1;

}

// 2. 64 轮压缩

uint32_t a=h_0, b=h_1, c=h_2, d=h_3,

e=h_4, f=h_5, g=h_6, h=h_7;

for (int i = 0; i < 64; ++i) {

uint32_t S1 = rotr(e,6) ^ rotr(e,11) ^ rotr(e,25);

uint32_t ch = (e & f) ^ (~e & g);

uint32_t t1 = h + S1 + ch + Ki + wi;

uint32_t S0 = rotr(a,2) ^ rotr(a,13) ^ rotr(a,22);

uint32_t maj = (a & b) ^ (a & c) ^ (b & c);

h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2;

}

h_0+=a; h_1+=b; h_2+=c; h_3+=d;

h_4+=e; h_5+=f; h_6+=g; h_7+=h;

}

};

```

**密码哈希**(盐值 + SHA-256):

```cpp

// 存储格式:16字节hex盐 : 64字符hex哈希

std::string hash_password(const std::string& password) {

std::string salt = random_hex(16); // 128-bit 随机盐

return salt + ":" + sha256(salt + password);

}

bool verify_password(const std::string& password, const std::string& stored) {

auto pos = stored.find(':');

std::string salt = stored.substr(0, pos);

return sha256(salt + password) == stored.substr(pos + 1);

}

```

4.4 MySQL RAII 连接池

```cpp

// db.h --- 连接池结构

class Pool {

public:

static Pool& instance() { // 单例

static Pool pool;

return pool;

}

std::unique_ptr<Connection> acquire() {

// 先尝试复用池中连接

if (!pool_.empty()) {

auto conn = std::move(pool_.back());

pool_.pop_back();

if (conn->ping()) return conn; // 连接存活 → 复用

}

// 池空或连接已断 → 新建

auto conn = std::make_unique<Connection>();

conn->connect();

return conn;

}

void release(std::unique_ptr<Connection> conn) {

if (pool_.size() < POOL_SIZE) {

pool_.push_back(std::move(conn)); // 归还池中

}

// 超过池大小 → 自动析构关闭

}

};

```

**设计要点**:

  • `Connection` 析构自动 `mysql_close()`,配合 `unique_ptr` 不会泄漏

  • `acquire()` 先 `ping()` 再复用,防止长连接断开

  • `escape()` 封装 `mysql_real_escape_string()`,所有 SQL 拼接都用它防注入

4.5 HTTP API 路由

```cpp

// server.h --- 路由注册(12 个 API 端点)

svr.Post("/api/auth/register", handler_register);

svr.Post("/api/auth/login", handler_login);

svr.Get("/api/problems", handler_problem_list);

svr.Get("/api/problems/(\\d+)", handler_problem_detail); // 正则匹配 ID

svr.Post("/api/submissions", handler_submit);

// ...共 12 个端点

// 认证守卫宏

#define AUTH_GUARD(req, res) \

auto* session = get_session(req); \

if (!session) { \

res.set_content(json::error("Authentication required"), "application/json"); \

return; \

}

// 管理员守卫宏

#define ADMIN_GUARD(req, res) \

AUTH_GUARD(req, res); \

if (!session->is_admin) { ... }

```

**设计亮点**:用宏来实现声明式权限检查,业务代码只需一行 `AUTH_GUARD(req, res)`,避免在每个 handler 里重复写认证逻辑。

4.6 零依赖 JSON 构建器

不引入任何 JSON 库,用 Builder 模式手工构建:

```cpp

// utils.h --- JSON Object Builder

json::Object resp;

resp.add("id", problem_id);

resp.add("title", "A + B Problem");

resp.add("difficulty", "easy");

resp.add_raw("testcases", all_arr.str()); // 嵌入子数组

return resp.str(); // → {"id":1,"title":"A + B Problem",...}

```

**设计要点**:`add_raw()` 允许直接插入已序列化的 JSON 片段,支持嵌套对象和数组,避免递归构建的复杂性。


5. 安全性设计

5.1 进程隔离

| 威胁 | 防护 |

|------|------|

| 恶意代码 `while(1){}` | `setrlimit(RLIMIT_CPU)` → SIGXCPU |

| 恶意代码 `sleep(100)` | `alarm()` 挂钟超时 |

| 恶意代码 `new int1e9` | `setrlimit(RLIMIT_AS)` → SIGKILL |

| Fork 炸弹 | `setrlimit(RLIMIT_NPROC, 1)` |

| 文件系统破坏 | I/O 全部通过 pipe,不直接访问磁盘 |

| 读取期望输出 | 子进程看不到数据库,只能在隔离目录执行 |

5.2 SQL 注入防护

```cpp

// ✅ 安全:参数化拼接 + escape

db->execute("INSERT INTO users (username, password_hash) VALUES ('"

  • db->escape(username) + "', '" + db->escape(hash) + "')");

// ✅ 安全:problem_id 先做 int 验证

int problem_id = std::stoi(req.matches1); // 非数字直接抛异常

// ❌ 绝对避免的做法

// std::string sql = "SELECT * FROM users WHERE username='" + username + "'";

```

5.3 XSS 防护

```javascript

// 前端:所有用户输入渲染前转义

function escapeHtml(str) {

const div = document.createElement('div');

div.textContent = str;

return div.innerHTML;

}

```

5.4 Session 管理

  • Token:128-bit 随机 hex 字符串

  • 存储:内存 `unordered_map`,过期自动清理

  • 传输:`Authorization: Bearer <token>` header


6. 部署运维

6.1 一键启动

```bash

必须从项目根目录启动(STATIC_DIR 是相对路径)!

cd ~/project

nohup ./backend/build/oj_backend > /tmp/oj.log 2>&1 &

验证

ss -tlnp | grep 8080

```

6.2 首次部署流程

```bash

1. 安装依赖

sudo apt install -y build-essential cmake g++ mysql-server libmysqlclient-dev

2. 下载 cpp-httplib

cd backend/third_party

curl -o cpp-httplib.h https://raw.githubusercontent.com/yhirose/cpp-httplib/master/httplib.h

3. 改数据库密码

vim backend/src/config.h # 改 DB_PASS

4. 初始化数据库

sudo mysql -u root -p < database/init.sql

5. 编译

cd backend && mkdir build && cd build && cmake .. && make -j$(nproc)

6. 启动(在项目根目录!)

cd ~/project && nohup ./backend/build/oj_backend > /tmp/oj.log 2>&1 &

7. 设管理员

mysql -u root -p -e "USE oj_platform; UPDATE users SET is_admin=1 WHERE username='你的用户名';"

```

> ⚠️ **设为管理员后必须退出重新登录**,因为 Session 缓存了旧的 `is_admin` 值。

6.3 阿里云安全组

别忘了在阿里云控制台 → 安全组 → 入方向规则中放行 **8080** 端口,否则外网访问会是 `ERR_CONNECTION_REFUSED`。


7. 踩坑记录

这是实际开发中遇到的所有问题,按时间顺序记录:

| # | 问题 | 根因 | 解决方案 |

|---|------|------|---------|

| 1 | `my_bool` 未声明 | MySQL 8.0 废弃了 `my_bool` 类型 | 改为 `bool` |

| 2 | `undefined reference to SSL_*` | cpp-httplib v0.7+ 需要链接 OpenSSL | CMake 加 `ssl crypto` |

| 3 | cpp-httplib.h 第一行 `404: Not Found` | curl URL 写错(`single_include` 分支) | 用 `master/httplib.h` |

| 4 | `Failed to connect to MySQL` | `config.h` 中 `DB_PASS` 为空字符串 | 改为真实密码,**重编译** |

| 5 | 启动后端口没监听 | 没在项目根目录启动(`STATIC_DIR` 是相对路径) | 必须 `cd ~/project` |

| 6 | `Exit 127` | build 目录被删,二进制不存在 | 重新 `cmake .. && make` |

| 7 | SIGALRM 杀掉整个后端 | `alarm()` 无 handler → 进程收到 SIGALRM 默认终止 | 安装空 handler + 不设 `SA_RESTART` |

| 8 | SHA-256 shell 命令注入 | `popen("echo -n '" + input + "' \| sha256sum")` | 纯 C++ 实现 SHA-256 |

| 9 | 设为管理员后仍提示无权限 | Session 内存缓存了旧 `is_admin` | 退出重新登录 |

| 10 | `Pages is not defined` | JS 加载顺序问题 | `var Pages = {};` 必须在页面脚本之前 |

| 11 | `this._renderFullPage is not a function` | 方法引用传参时 `this` 丢失 | 用箭头函数包裹 |


附录:判题状态说明

| 状态码 | 含义 | 触发条件 |

|--------|------|---------|

| AC (Accepted) | 通过 | 所有用例输出匹配 |

| WA (Wrong Answer) | 答案错误 | 输出与预期不一致 |

| TLE (Time Limit Exceeded) | 超时 | CPU 时限或挂钟时限到达 |

| MLE (Memory Limit Exceeded) | 内存超限 | 超过内存限制被 SIGKILL |

| RE (Runtime Error) | 运行错误 | 非零退出码/SIGSEGV/SIGFPE |

| CE (Compilation Error) | 编译错误 | g++ 编译失败 |


> 项目地址:GitHub `Qingfeng-Blip` | 部署于阿里云 ECS 8.134.126.29:8080

相关推荐
2401_894915531 小时前
Geo 优化源码部署常见报错排查:连接失败、地图加载、地域词失效解决方案
服务器·开发语言·python·安全·php
c238561 小时前
C/C++每日一练20
c语言·开发语言·c++
DFT计算杂谈1 小时前
C4T交错磁体中的超导交织序向列涨落与自旋流环涨落的选择机制
大数据·数据库·人工智能
2601_965798472 小时前
My Honest Review of Rank Math SEO Pro for Beginner Sites
数据库·人工智能·php
starzy19902 小时前
Soloop 智能视频创作全流程实战指南
数据库·microsoft·音视频
2601_963282772 小时前
极寒场景专网通信技术实践:黑龙江零下 40℃环境数字对讲组网方案落地解析
大数据·运维·数据库
可爱系程序猿2 小时前
msvcp140_codecvt_ids.dll 缺失的排查记录:C++ 程序启动异常时如何核对运行库版本
开发语言·c++·程序人生·电脑
weixin_440784112 小时前
【IntentSeivice实现原理】
android·java·开发语言·intentservice
encoconut2 小时前
01_C++ 入门基础
c++