[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-11611":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":14,"stars7d":14,"stars30d":14,"stars90d":14,"forks30d":14,"starsTrendScore":14,"compositeScore":15,"rankGlobal":9,"rankLanguage":9,"license":9,"archived":16,"fork":16,"defaultBranch":17,"hasWiki":18,"hasPages":16,"topics":19,"createdAt":9,"pushedAt":9,"updatedAt":20,"readmeContent":21,"aiSummary":22,"trendingCount":14,"starSnapshotCount":14,"syncStatus":23,"lastSyncTime":24,"discoverSource":25},11611,"AlgoBeatOnlineJudge","ZemuZzz\u002FAlgoBeatOnlineJudge","ZemuZzz","A modified SYZOJ-based online judge with tag library and solution review system.",null,"EJS",99,5,1,0,2.33,false,"main",true,[],"2026-06-12 02:02:32","# AlgoBeat Online Judge\n\n基于 [SYZOJ](https:\u002F\u002Fgithub.com\u002Fsyzoj\u002Fsyzoj) 二次开发的中文 OJ 评测 + 社区平台。在原版基础上重构 UI 框架,新增了完整的**社区互动**、**通知中心**、**关注\u002F犇犇社交**、**工单**、**Hit 值评分**、**用户名牌子**、**邮箱验证**、**剪贴板 Markdown 编辑器**等数十个模块。\n\n> **当前版本**:v1.7.1 · 📋 剪贴板 Markdown 实时预览编辑器 · 全本地化静态资源\n\n近期主要里程碑(完整历史见 [#版本历史](#-版本历史)):\n\n- **v1.7.1** · 剪贴板编辑器升级 - 左右分屏 + 实时预览 + 代码高亮 + KaTeX\n- **v1.7.0** · 关注\u002F粉丝系统 + 犇犇板块 + @ 提及通知\n- **v1.6.0** · 通知中心 + 左侧 Sidebar UI 重构 + Banner 轮播系统\n- **v1.5.x** · 用户名牌子 + 作弊者标签 + 提交记录管理员标记\n\n## ✨ 主要特性\n\n### 🎨 整体 UI(v1.6.0 重构)\n\n参考洛谷的两栏布局,告别 SYZOJ 原版的顶部横向菜单:\n\n- **顶部 topbar**(50px 高):logo + 站名 + 通知铃铛 + 站内信 + 用户头像\n- **左侧 sidebar**(220px 宽):垂直导航(首页\u002F题库\u002F比赛\u002F评测\u002F排名\u002F讨论\u002F标签\u002F工单\u002F帮助)\n- **折叠按钮**:点 ☰ 收起到 56px,localStorage 持久化偏好\n- **FOUC 防护**:HTML 顶部内联 script 抢先应用折叠状态,无闪烁\n- **Contest 页**:topbar 左侧固定「返回比赛」按钮\n- **admin 菜单**:仍在用户头像 dropdown(后台\u002F题解审核\u002F公告管理\u002F牌子管理\u002FBanner 管理\u002F重算 Hit\u002F重启服务)\n\n> ⚠️ 设计取舍:v1.6.0 取消了移动端响应式,站点固定按 1200px 桌面宽度渲染。手机用户横向滚动查看 - 我们认为这比强行压缩页面更好。\n\n### 🔔 通知中心(v1.6.0)\n\n完全独立于站内信的通知系统:\n\n- 列表页 `\u002Fnotifications` 支持全部\u002F未读切换、单条已读、全部已读、删除\n- header 铃铛 + **未读数 badge**\n- 触发场景:\n  - 题解审核**通过** \u002F **拒绝** → 通知作者\n  - 工单**公开回复**(非内部备注) → 通知工单创建者\n  - 工单**状态变更** → 通知工单创建者\n  - 题解评论 → 通知题解作者\n  - 题解\u002F犇犇里 @ 提及 → 通知被提及者\n  - 犇犇被回复 → 通知作者\n- 内置去重逻辑:`actor === recipient` 自动跳过\n\n提供全局 API `syzoj.utils.createNotification(opts)` 给其他模块使用。\n\n### 👥 关注\u002F粉丝系统(v1.7.0)\n\n单向关注模型(像微博\u002FTwitter,无需对方同意):\n\n- 用户主页头部「**关注 N | 粉丝 M**」(可点击进入完整列表)\n- 关注按钮三态:\n  - **+ 关注**(蓝)→ 没关注过\n  - **✓ 已关注**(灰)→ 单向关注\n  - **↔ 互相关注**(绿)→ 互关\n- 列表页 `\u002Fuser\u002F\u003Cid>\u002Ffollowing` 和 `\u002Fuser\u002F\u003Cid>\u002Ffollowers`\n- 取关需要 confirm;不能关注自己;无关注上限\n- 关注事件**不**通知被关注者(保持安静)\n\n中间件 `_user_follow_loader.js` 在 `\u002Fuser\u002F:id*` 路由前自动注入 `res.locals.followStats` + `followRelation`。\n\n### 💬 犇犇板块(v1.7.0)\n\n类微博的短文社交,深度集成关注\u002F通知系统:\n\n- **发布**:500 字以内文字 + 最多 **9 张图**(JPG\u002FPNG\u002FWebP\u002FGIF 各 5MB);`Ctrl+Enter` 快速发送\n- **回复**:单层结构,嵌套自动 fold 回原创\n- **可见性规则**:\n  - 作者本人 + **关注作者者** + admin\u002Fmanage_user 权限者可见\n  - **取关后立即不可见**(查询时实时判断)\n- **删除**:软删除(`is_deleted=1`),回复保留;作者本人 + admin\u002Fmanage_user 可删\n- **信息流 tabs**:我关注的 \u002F 我发布的 \u002F 全部(管理 - 仅 admin)\n- 文字内 `@username` 自动转链接,触发被提及通知\n- 用户名渲染跟全站一致(颜色 + 牌子 tag)\n- 首页公告下方集成 AJAX feed\n\n### 📋 个人剪贴板(v1.7.1 升级)\n\n代码模板 \u002F 笔记 \u002F 题解草稿的私人空间,Markdown 实时预览:\n\n- **左右分屏编辑器**:左侧 Markdown 编辑 + 右侧实时预览\n- **代码高亮**:highlight.js(支持 C\u002FC++\u002FPython\u002FJS\u002FGo 等主流语言)\n- **数学公式**:KaTeX($E = mc^2$、行内公式、块级公式、矩阵)\n- 滚动同步、字数 \u002F 行数实时统计\n- 三种可见性:\n  - **私有**:仅自己可见\n  - **公开**:出现在用户公开列表\n  - **分享链接**:凭 token 访问,可设过期时间(0 = 永久)\n- 单条内容 100 KB 上限\n- **全本地化**:markdown-it + highlight.js + KaTeX 库**全部下载到自家服务器**(`\u002Fself\u002Flib\u002F`),不依赖任何 CDN\n\n### 🏷️ 用户名牌子系统(v1.5.0)\n\n参考洛谷设计的荣誉徽章:\n\n- **管理员自动获得**:拥有 is_admin 或 manage_problem\u002Fmanage_problem_tag\u002Fmanage_user 任一权限的用户**自动**获得 tag 权限,默认显示「管理员」\n- **手动授权**:超级管理员可在 `\u002Fadmin\u002Fuser-tags` 管理界面授予普通用户 tag 权限\n- **个性化设置**:用户可在 `\u002Fedit` 自定义 tag 文字(最多 12 字符)和展示开关\n- **颜色一致**:tag 颜色与用户名颜色档完全一致(管理员紫、Hit 值红\u002F橙\u002F绿\u002F蓝\u002F灰)\n- **管理权限取消后** tag 权限保留;**超级管理员**可撤销任意非超管用户的 tag 权限\n- **审计完整**:所有授权\u002F撤销操作记录授权超管 ID + 时间戳\n\n### 🛡️ 作弊者标签(v1.5.0)\n\n惩罚维度上的\"标签\",跟荣誉 tag 是两个独立系统:\n\n- 任何用户**至少有一条 `judge_state_admin_action.action_type='cheated'` 记录**时,自动:\n  - 用户名颜色变为**棕色 + 删除线**(覆盖所有 tier)\n  - 强制显示「作弊者」棕色 tag(覆盖任何荣誉 tag)\n- **作弊判定撤销后**自动恢复(60 秒内缓存刷新)\n- **管理员豁免**:管理员被标记 cheated 时无任何视觉变化\n- **荣誉 tag 不消失**:即使被授权用户被标 cheater,他在 \u002Fedit 仍可设置 tag,作弊状态被撤销后立即恢复\n\n### 🎫 工单系统(v1.4.0)\n\n参照洛谷工单设计,覆盖 6 大类别:\n\n| 大类 | 子类型 |\n|---|---|\n| 题目工单 | 题目综合 \u002F 文本修缮 \u002F 改进标签、难度 |\n| 比赛工单 | 申请公开赛 |\n| 文章工单 | 申请题解相关 \u002F 撤销题解相关 |\n| 用户工单 | 用户申诉 \u002F 申请权限变更 \u002F 申请解除封禁 |\n| 举报工单 | 举报用户(强制要求填写举报原因) |\n| 综合问题 | 建议或 bug 反馈 \u002F 学术建议 \u002F 一般咨询 |\n\n支持附件上传(单文件 20MB,每工单最多 10 个附件,任意类型),支持 admin 内部备注、用户撤回、5 状态流转、24h 5 个工单频率限制。**回复 \u002F 状态变更触发通知中心通知。**\n\n### 🛡️ 提交记录管理员标记(v1.4.0)\n\n管理员可在任意提交详情页**标记**为「作弊」或「已取消」,自动:\n\n- 同步 ac_num 增减\n- 排除 Hit 值计算\n- 触发用户名牌子的\"作弊者\"识别\n- 完全可撤销(撤销时反向修正)\n\nv1.5.1 加入「重新评测」按钮,可对单条提交重测。\n\n### 🎯 Hit 值评分系统(v1.3.x)\n\n平台用一套加权公式(满分 400)刻画每位用户的综合活跃度:\n\n| 维度 | 满分 | 主要因素 |\n|---|---|---|\n| 基础信用分 | 100 | 邮箱验证、信息完善、注册时长、参赛门槛 |\n| 社区贡献分 | 100 | 通过审核的题解数、出题数 |\n| 比赛参与分 | 100 | 参赛活跃度(30 天半衰减) |\n| 题目练习分 | 100 | AC 题目数、知识点覆盖(14 天半衰减) |\n\n每天凌晨自动重算 + 管理员可手动触发即时重算。用户主页 Hit 值卡片支持隐藏。\n\n### 🎨 用户名颜色分档(v1.3.2)\n\n| Hit 值 | 颜色 | 称号 |\n|---|---|---|\n| 0–99 | 灰 | 萌新 |\n| 100–199 | 蓝 | 业余 |\n| 200–279 | 绿 | 进阶 |\n| 280–349 | 橙 | 高手 |\n| 350–400 | 红 | 大神 |\n| — | 紫 | 站点管理者 |\n| — | 棕(删除线) | 作弊者 |\n\n颜色全站统一渲染,包括 SYZOJ 自带页面 + Vue 动态组件。\n\n### 🖼️ 首页改版(v1.6.0)\n\n参考洛谷的\"门户化\"首页:\n\n- 顶部:**Banner 轮播**(左 2\u002F3 宽) + 右侧栏「近期比赛」+「一言」\n- 公告区(底部左 2\u002F3 宽,带样式分级)\n- 公告下方:**犇犇 feed widget**(发布表单 + tabs + AJAX 列表)\n- 右侧栏:搜索题目 + 友情链接\n\n### 🎯 Banner 轮播 + 后台管理(v1.6.0)\n\n`\u002Fadmin\u002Fbanners` 完整管理界面:\n\n- 上传图片:JPG \u002F PNG \u002F WebP \u002F GIF 各 5MB\n- 配置:标题、可选**跳转链接**、排序、启用开关、生效时间段\n- 留空跳转链接则点击不跳转\n- 首页轮播:多张图 5 秒\u002F张自动播放、鼠标 hover 暂停、左右箭头切换、底部圆点定位\n- 图片存储:`\u002Fapp\u002Fstatic\u002Fself\u002Fbanner`(host bind-mount 到 `.\u002Fcustom\u002Fuploads\u002Fbanner`)\n\n### 📊 排名页双 Tab\n\n`\u002Franklist` 顶部支持切换 **Rating 排名** 与 **Hit 值排名**,左侧均显示用户头像(v1.5.0 新增)。\n\n### 📈 Hit 值历史趋势\n\n用户主页绘制 4 维度近 30 天趋势折线图,使用 Chart.js。历史数据保留 90 天。\n\n### 📑 标签库浏览\n\n顶部「标签」入口,按颜色分类展示站内全部标签。\n\n### 📝 题解系统(v1.2.1 起,v1.6.0 补充审核员显示)\n\n- 用户可投稿题解,支持 Markdown\n- 投稿默认进入审核状态\n- 审核者可在 `\u002Fadmin\u002Fsolutions` 集中处理\n- 审核者可按题禁止\u002F恢复题解投稿\n- **审核员信息公开化**(v1.6.0):\n  - 题解详情页:显示「**审核员**: xxx · 审核时间」\n  - 管理员审核列表:增加「**处理人**」列\n- 题解评论支持 @ 提及(v1.7.0)\n- 评论自动通知题解作者(v1.7.0)\n\n### 📢 站内公告系统\n\n- 三个级别(信息 \u002F 警告 \u002F 重要)\n- 用户访问首页自动弹窗\n- 「不再弹出」选项(按浏览器 localStorage 记录)\n\n### 💬 站内信(v1.3.1)\n\n- 一对一私信,支持 Markdown\n- 右上角信封图标显示未读数\n- 用户可关闭来自他人的私信\n\n### 📧 邮箱验证(v1.3.2)\n\n- 通过 SMTP(默认 Zoho)发送验证邮件\n- 24 小时有效期 + 60 秒发送冷却\n- 未验证用户不能投稿题解、发送站内信\n\n### 🛠 站点定制与体验改进\n\n- 容器全部 `TZ=Asia\u002FShanghai`\n- 修复 RabbitMQ healthcheck 超时启动失败\n- 修复 cgroup v2 系统下 judge runner 启动失败\n- 主页左上角自定义 logo + 文字\n- 自定义 favicon + 页脚(含 ICP 备案号、版本号、GitHub 链接)\n- gravatar 头像源切换至更稳定镜像\n- **静态资源全本地化**(v1.7.1):markdown-it + highlight.js + KaTeX 直接挂载在 `\u002Fself\u002Flib\u002F`\n\n## 🚀 快速部署\n\n### 系统要求\n\n- Linux 服务器(推荐 Ubuntu 22.04+)\n- Docker 20.10+ 和 Docker Compose v2\n- **必须使用 cgroup v1**(judge runner 依赖 simple-sandbox,不兼容 cgroup v2)\n\n如果你的系统默认是 cgroup v2,编辑 `\u002Fetc\u002Fdefault\u002Fgrub`,在 `GRUB_CMDLINE_LINUX` 追加 `systemd.unified_cgroup_hierarchy=0`,然后 `update-grub && reboot`。\n\n### 部署步骤\n\n```bash\n# 1. 克隆仓库\ngit clone git@github.com:ZemuZzz\u002FAlgoBeatOnlineJudge.git\ncd AlgoBeatOnlineJudge\n\n# 2. 生成密钥配置\ncp env-app.example env-app\nvim env-app\n\n# 3. 安装 nodemailer 依赖(用于邮箱验证)\nmkdir -p custom\u002Fnode_modules\ndocker run --rm -v $(pwd)\u002Fcustom:\u002Fwork -w \u002Fwork node:14 npm install nodemailer@6.9.7\n\n# 4. 创建运行时上传目录\nmkdir -p custom\u002Fuploads\u002Ftickets\nmkdir -p custom\u002Fuploads\u002Fbanner\nmkdir -p custom\u002Fuploads\u002Fbenben\n\n# 5. 下载本地化静态库(v1.7.1)\nmkdir -p custom\u002Fstatic-libs\u002Ffonts\ncd \u002Ftmp\n# markdown-it\ncurl -L -o md.tgz https:\u002F\u002Fregistry.npmmirror.com\u002Fmarkdown-it\u002F-\u002Fmarkdown-it-14.1.0.tgz\ntar xzf md.tgz && cp package\u002Fdist\u002Fmarkdown-it.min.js ..\u002Fcustom\u002Fstatic-libs\u002Fmarkdown-it.min.js && rm -rf package md.tgz\n# highlight.js\ncurl -L -o hl.tgz https:\u002F\u002Fregistry.npmmirror.com\u002F@highlightjs\u002Fcdn-assets\u002F-\u002Fcdn-assets-11.10.0.tgz\ntar xzf hl.tgz\ncp package\u002Fhighlight.min.js ..\u002Fcustom\u002Fstatic-libs\u002Fhighlight.min.js\ncp package\u002Fstyles\u002Fatom-one-light.min.css ..\u002Fcustom\u002Fstatic-libs\u002Fhighlight-atom-one-light.min.css\nrm -rf package hl.tgz\n# katex\ncurl -L -o katex.tgz https:\u002F\u002Fregistry.npmmirror.com\u002Fkatex\u002F-\u002Fkatex-0.16.11.tgz\ntar xzf katex.tgz\ncp package\u002Fdist\u002Fkatex.min.js ..\u002Fcustom\u002Fstatic-libs\u002Fkatex.min.js\ncp package\u002Fdist\u002Fkatex.min.css ..\u002Fcustom\u002Fstatic-libs\u002Fkatex.min.css\ncp package\u002Fdist\u002Fcontrib\u002Fauto-render.min.js ..\u002Fcustom\u002Fstatic-libs\u002Fkatex-auto-render.min.js\ncp -r package\u002Fdist\u002Ffonts\u002F* ..\u002Fcustom\u002Fstatic-libs\u002Ffonts\u002F\nrm -rf package katex.tgz\ncd -\n\n# 6. 启动服务\ndocker compose up -d\n\n# 7. 创建管理员账号(注册账号后)\ndocker exec -i algobeat-mariadb-1 mariadb -u root syzoj \\\n  -e \"UPDATE \\`user\\` SET is_admin = 1 WHERE username = '你的用户名';\"\n\n# 8. 初始化业务数据表(见下方 SQL 脚本)\n```\n\n### SQL 初始化脚本\n\n```bash\ndocker exec -i algobeat-mariadb-1 mariadb -u root syzoj \u003C\u003C'EOF'\n-- ============ v1.2.x: 题解 \u002F 评论 \u002F 投稿开关 ============\nCREATE TABLE IF NOT EXISTS `problem_solution` (\n  `id` INT NOT NULL AUTO_INCREMENT, `title` VARCHAR(80) DEFAULT NULL,\n  `content` MEDIUMTEXT DEFAULT NULL, `problem_id` INT DEFAULT NULL,\n  `user_id` INT DEFAULT NULL, `status` VARCHAR(20) DEFAULT 'pending',\n  `public_time` INT DEFAULT NULL, `update_time` INT DEFAULT NULL,\n  `reject_reason` VARCHAR(255) DEFAULT NULL, `allow_comment` BOOLEAN DEFAULT TRUE,\n  `comments_num` INT DEFAULT 0,\n  `reviewer_id` INT DEFAULT NULL, `reviewed_at` INT DEFAULT NULL,  -- v1.6.0 新增\n  PRIMARY KEY (`id`),\n  KEY `idx_problem_id` (`problem_id`), KEY `idx_user_id` (`user_id`),\n  KEY `idx_status` (`status`), KEY `idx_problem_status` (`problem_id`, `status`),\n  KEY `idx_reviewer` (`reviewer_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `problem_solution_comment` (\n  `id` INT NOT NULL AUTO_INCREMENT, `content` TEXT DEFAULT NULL,\n  `solution_id` INT DEFAULT NULL, `user_id` INT DEFAULT NULL,\n  `public_time` INT DEFAULT NULL, PRIMARY KEY (`id`),\n  KEY `idx_solution_id` (`solution_id`), KEY `idx_user_id` (`user_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `problem_solution_setting` (\n  `problem_id` INT NOT NULL, `disable_submission` BOOLEAN DEFAULT FALSE,\n  `update_time` INT DEFAULT NULL, `updated_by` INT DEFAULT NULL,\n  PRIMARY KEY (`problem_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\n-- ============ v1.3.x: 公告 \u002F 站内信 \u002F 剪贴板 \u002F 邮箱验证 \u002F Hit 值 ============\nCREATE TABLE IF NOT EXISTS `announcement` (\n  `id` INT NOT NULL AUTO_INCREMENT, `title` VARCHAR(120) DEFAULT NULL,\n  `content` MEDIUMTEXT DEFAULT NULL, `level` VARCHAR(20) DEFAULT 'info',\n  `start_time` INT DEFAULT NULL, `end_time` INT DEFAULT NULL,\n  `is_active` BOOLEAN DEFAULT TRUE, `public_time` INT DEFAULT NULL,\n  `update_time` INT DEFAULT NULL, PRIMARY KEY (`id`),\n  KEY `idx_active_time` (`is_active`, `start_time`, `end_time`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `private_message` (\n  `id` INT NOT NULL AUTO_INCREMENT, `sender_id` INT DEFAULT NULL,\n  `receiver_id` INT DEFAULT NULL, `content` TEXT DEFAULT NULL,\n  `public_time` INT DEFAULT NULL, `is_read` BOOLEAN DEFAULT FALSE,\n  `sender_deleted` BOOLEAN DEFAULT FALSE, `receiver_deleted` BOOLEAN DEFAULT FALSE,\n  PRIMARY KEY (`id`), KEY `idx_sender` (`sender_id`),\n  KEY `idx_receiver` (`receiver_id`), KEY `idx_pair` (`sender_id`, `receiver_id`),\n  KEY `idx_unread` (`receiver_id`, `is_read`, `receiver_deleted`),\n  KEY `idx_time` (`public_time`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `user_message_setting` (\n  `user_id` INT NOT NULL, `disable_messages` BOOLEAN DEFAULT FALSE,\n  `update_time` INT DEFAULT NULL, PRIMARY KEY (`user_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `clipboard_item` (\n  `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT DEFAULT NULL,\n  `title` VARCHAR(120) DEFAULT NULL, `content` MEDIUMTEXT DEFAULT NULL,\n  `visibility` VARCHAR(20) DEFAULT 'private', `share_token` VARCHAR(40) DEFAULT NULL,\n  `share_expires` INT DEFAULT NULL, `public_time` INT DEFAULT NULL,\n  `update_time` INT DEFAULT NULL, PRIMARY KEY (`id`),\n  KEY `idx_user_id` (`user_id`), KEY `idx_visibility` (`visibility`),\n  UNIQUE KEY `uniq_share_token` (`share_token`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `email_verification_token` (\n  `token` VARCHAR(64) NOT NULL, `user_id` INT NOT NULL,\n  `email` VARCHAR(120) DEFAULT NULL, `purpose` VARCHAR(20) DEFAULT 'register',\n  `created_at` INT DEFAULT NULL, `expires_at` INT DEFAULT NULL,\n  `used` BOOLEAN DEFAULT FALSE, PRIMARY KEY (`token`),\n  KEY `idx_user_id` (`user_id`), KEY `idx_expires` (`expires_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `user_email_status` (\n  `user_id` INT NOT NULL, `is_email_verified` BOOLEAN DEFAULT FALSE,\n  `verified_at` INT DEFAULT NULL, `last_send_at` INT DEFAULT NULL,\n  PRIMARY KEY (`user_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `user_hit_score` (\n  `user_id` INT NOT NULL, `total` INT DEFAULT 0,\n  `basic_score` INT DEFAULT 0, `contribution_score` INT DEFAULT 0,\n  `contest_score` INT DEFAULT 0, `practice_score` INT DEFAULT 0,\n  `last_calc_at` INT DEFAULT NULL, PRIMARY KEY (`user_id`),\n  KEY `idx_total` (`total`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `user_hit_score_history` (\n  `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL,\n  `total` INT DEFAULT 0, `basic_score` INT DEFAULT 0,\n  `contribution_score` INT DEFAULT 0, `contest_score` INT DEFAULT 0,\n  `practice_score` INT DEFAULT 0, `recorded_at` INT NOT NULL,\n  PRIMARY KEY (`id`), KEY `idx_user_time` (`user_id`, `recorded_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `user_hit_setting` (\n  `user_id` INT NOT NULL, `hide_hit` BOOLEAN DEFAULT FALSE,\n  `update_time` INT DEFAULT NULL, PRIMARY KEY (`user_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\n-- ============ v1.4.x: 工单 \u002F 提交记录管理员标记 ============\nCREATE TABLE IF NOT EXISTS `judge_state_admin_action` (\n  `judge_id` INT NOT NULL, `action_type` VARCHAR(20) NOT NULL,\n  `operator_id` INT NOT NULL, `operator_time` INT NOT NULL,\n  `reason` VARCHAR(255) DEFAULT NULL, `was_accepted` BOOLEAN DEFAULT FALSE,\n  `affected_problem_id` INT DEFAULT NULL, `affected_user_id` INT DEFAULT NULL,\n  PRIMARY KEY (`judge_id`),\n  KEY `idx_user_action` (`affected_user_id`, `action_type`),\n  KEY `idx_problem_user` (`affected_problem_id`, `affected_user_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `ticket` (\n  `id` INT NOT NULL AUTO_INCREMENT, `category` VARCHAR(20) NOT NULL,\n  `subtype` VARCHAR(60) NOT NULL, `title` VARCHAR(200) NOT NULL,\n  `description` MEDIUMTEXT, `creator_id` INT NOT NULL,\n  `assignee_id` INT DEFAULT NULL, `status` VARCHAR(20) DEFAULT 'pending',\n  `relation_type` VARCHAR(20) DEFAULT NULL, `relation_id` INT DEFAULT NULL,\n  `extra_data` TEXT, `is_public` BOOLEAN DEFAULT FALSE,\n  `created_at` INT NOT NULL, `updated_at` INT NOT NULL,\n  PRIMARY KEY (`id`), KEY `idx_creator` (`creator_id`),\n  KEY `idx_status` (`status`), KEY `idx_category` (`category`),\n  KEY `idx_assignee` (`assignee_id`),\n  KEY `idx_relation` (`relation_type`, `relation_id`),\n  KEY `idx_updated_at` (`updated_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `ticket_reply` (\n  `id` INT NOT NULL AUTO_INCREMENT, `ticket_id` INT NOT NULL,\n  `user_id` INT NOT NULL, `content` MEDIUMTEXT NOT NULL,\n  `is_internal` BOOLEAN DEFAULT FALSE, `is_status_change` BOOLEAN DEFAULT FALSE,\n  `created_at` INT NOT NULL, PRIMARY KEY (`id`),\n  KEY `idx_ticket` (`ticket_id`), KEY `idx_user` (`user_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `ticket_attachment` (\n  `id` INT NOT NULL AUTO_INCREMENT, `ticket_id` INT NOT NULL,\n  `reply_id` INT DEFAULT NULL, `uploader_id` INT NOT NULL,\n  `filename` VARCHAR(255) NOT NULL, `original_name` VARCHAR(255) NOT NULL,\n  `file_size` INT NOT NULL, `mime_type` VARCHAR(120),\n  `created_at` INT NOT NULL, PRIMARY KEY (`id`),\n  KEY `idx_ticket` (`ticket_id`), KEY `idx_reply` (`reply_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\n-- ============ v1.5.x: 用户名牌子 ============\nCREATE TABLE IF NOT EXISTS `user_tag` (\n  `user_id` INT NOT NULL,\n  `tag_text` VARCHAR(12) DEFAULT '',\n  `is_visible` BOOLEAN DEFAULT TRUE,\n  `granted_by` INT DEFAULT NULL, `granted_at` INT DEFAULT NULL,\n  `is_disabled` BOOLEAN DEFAULT FALSE,\n  `disabled_by` INT DEFAULT NULL, `disabled_at` INT DEFAULT NULL,\n  `disabled_reason` VARCHAR(255) DEFAULT NULL,\n  `updated_at` INT DEFAULT NULL, PRIMARY KEY (`user_id`),\n  KEY `idx_disabled` (`is_disabled`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\n-- ============ v1.6.0: 通知中心 + Banner ============\nCREATE TABLE IF NOT EXISTS `notification` (\n  `id` INT NOT NULL AUTO_INCREMENT,\n  `recipient_id` INT NOT NULL,\n  `type` VARCHAR(50) NOT NULL,\n  `title` VARCHAR(255) NOT NULL,\n  `content` TEXT,\n  `source_url` VARCHAR(500),\n  `source_id` INT,\n  `actor_id` INT,\n  `is_read` TINYINT(1) DEFAULT 0,\n  `created_at` INT NOT NULL,\n  `read_at` INT,\n  PRIMARY KEY (`id`),\n  KEY `idx_recipient_unread` (`recipient_id`, `is_read`),\n  KEY `idx_recipient_created` (`recipient_id`, `created_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `homepage_banner` (\n  `id` INT NOT NULL AUTO_INCREMENT,\n  `title` VARCHAR(100) NOT NULL,\n  `image_path` VARCHAR(500) NOT NULL,\n  `link_url` VARCHAR(500),\n  `sort_order` INT DEFAULT 0,\n  `is_active` TINYINT(1) DEFAULT 1,\n  `start_time` INT, `end_time` INT,\n  `created_by` INT, `created_at` INT NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `idx_active_sort` (`is_active`, `sort_order`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\n-- ============ v1.7.0: 关注\u002F粉丝 + 犇犇 ============\nCREATE TABLE IF NOT EXISTS `user_follow` (\n  `id` INT NOT NULL AUTO_INCREMENT,\n  `follower_id` INT NOT NULL,\n  `followee_id` INT NOT NULL,\n  `created_at` INT NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uq_follow` (`follower_id`, `followee_id`),\n  KEY `idx_followee` (`followee_id`),\n  KEY `idx_follower` (`follower_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `benben_post` (\n  `id` INT NOT NULL AUTO_INCREMENT,\n  `user_id` INT NOT NULL,\n  `content` TEXT NOT NULL,\n  `reply_to` INT,\n  `is_deleted` TINYINT(1) DEFAULT 0,\n  `created_at` INT NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `idx_user_created` (`user_id`, `created_at`),\n  KEY `idx_reply_to` (`reply_to`),\n  KEY `idx_deleted` (`is_deleted`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE IF NOT EXISTS `benben_image` (\n  `id` INT NOT NULL AUTO_INCREMENT,\n  `post_id` INT NOT NULL,\n  `filename` VARCHAR(255) NOT NULL,\n  `original_name` VARCHAR(255),\n  `uploader_id` INT NOT NULL,\n  `file_size` INT,\n  `created_at` INT NOT NULL,\n  PRIMARY KEY (`id`),\n  KEY `idx_post` (`post_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\nEOF\n```\n\n### 启动后访问\n\n默认监听端口 `4567`:`http:\u002F\u002F你的服务器IP:4567`\n\n## 📁 仓库结构\n\n```\n.\n├── docker-compose.yml              # 主部署配置\n├── env-app.example                 # 密钥模板\n├── env                             # 项目级环境变量\n├── .gitignore\n├── README.md\n└── custom\u002F                         # 所有自定义代码与资源\n├── header.ejs                  # 顶部 topbar + sidebar 渲染入口\n├── sidebar.css \u002F sidebar.js    # v1.6.0 sidebar 布局 + 折叠逻辑\n├── mobile.css                  # v1.6.0 (空文件,移动端适配已取消)\n├── username_tiers.css          # 用户名颜色档 + tag 样式\n├── web.json                    # 站点配置\n├── favicon.png \u002F logo.png\n├── static-libs\u002F                # v1.7.1 本地化 npm 包\n│   ├── markdown-it.min.js\n│   ├── highlight.min.js + .css\n│   ├── katex.min.js + .css + auto-render\n│   └── fonts\u002F                  # KaTeX 字体\n├── uploads\u002F                    # 运行时上传\n│   ├── tickets\u002F                # v1.4.0 工单附件\n│   ├── banner\u002F                 # v1.6.0 banner 图片\n│   └── benben\u002F                 # v1.7.0 犇犇图片\n├── views\u002F                      # 页面模板(EJS)\n├── modules\u002F                    # 路由模块(JS)\n│   ├── _user_privilege_loader.js  # 中间件:注入 privileges 到 res.locals\n│   ├── _user_follow_loader.js     # v1.7.0 注入 follow 状态\n│   ├── _user_tag_loader.js        # 注入 tag 状态\n│   ├── _username_cache.js         # 全局用户名缓存(60s 刷新)\n│   ├── _username_renderer.js      # 全局 syzoj.utils.renderUsername\n│   ├── _user_tag.js               # tag 后台路由\n│   ├── __hit_score_engine.js      # Hit 值计算引擎\n│   ├── ticket.js                  # 工单系统\n│   ├── notification.js            # v1.6.0 通知中心\n│   ├── banner.js                  # v1.6.0 banner 后台\n│   ├── user_follow.js             # v1.7.0 关注\u002F粉丝\n│   ├── benben.js                  # v1.7.0 犇犇板块\n│   ├── solution.js                # 题解系统\n│   ├── message.js                 # 站内信\n│   └── clipboard.js               # 个人剪贴板\n├── models-built\u002F                  # 编译后的 typeorm model(.js)\n└── models\u002F                        # ts 占位文件(用于 SYZOJ readdirSync 注册)## 🔧 常用维护命令\n```\n\n```bash\n# 启动 \u002F 重启 \u002F 停止\ndocker compose up -d                       # 启动或应用配置变更\ndocker compose up -d --force-recreate web  # 改了挂载后强制重建\ndocker compose restart web                 # 仅重启 web(改 EJS\u002FJS 后用)\n\n# 查看状态和日志\ndocker logs --tail 100 algobeat-web-1\ndocker logs -f algobeat-web-1              # 实时跟随\n\n# 进容器调试\ndocker exec -it algobeat-web-1 bash\ndocker exec -it algobeat-mariadb-1 mariadb -u root syzoj\n```\n\n## 📦 版本历史\n\n完整版本说明详见 [Releases](https:\u002F\u002Fgithub.com\u002FZemuZzz\u002FAlgoBeatOnlineJudge\u002Freleases)。\n\n- **v1.7.1**:剪贴板 Markdown 编辑器升级·左侧编辑右侧实时预览·代码高亮·KaTeX 公式·查看页同步渲染·所有库本地化(自 host \u002Fself\u002Flib\u002F)\n- **v1.7.0**:关注\u002F粉丝系统(含互关标识)·犇犇板块(多图发布 + 回复 + @ 提及)·@ 提及通知(题解评论\u002F犇犇)·题解评论通知作者·首页集成犇犇 feed\n- **v1.6.0**:通知中心(铃铛 + 未读数 + 多触发器)·左侧 sidebar(洛谷风格)UI 重构·FOUC 防护·首页 banner 轮播系统(含 admin 后台)·首页重新设计·题解审核员显示\n- **v1.5.1**:单条提交操作完整化·重新评测按钮·作弊判定的完整后果(榜单清零 + 沉底 + Hit 值剔除作弊比赛)·取消评测严格实现·全局 success.ejs 模板\n- **v1.5.0**:用户名牌子系统·作弊者标签·排行榜头像·修复 admin-cache 漏 super admin bug\n- **v1.4.0**:工单系统(6 大类 + 附件上传)·比赛删除·提交记录作弊\u002F取消标记·关闭投稿确认框 bug 修复\n- **v1.3.4**:Hit 值系统完整收官·颜色档全店生效·近 30 天趋势折线图·`\u002Franklist` 双 Tab\n- **v1.3.3**:Hit 值计算引擎·用户主页卡片·隐藏开关·帮助页\n- **v1.3.2**:邮箱验证(Zoho SMTP)·用户名颜色档基础(紫色管理员)\n- **v1.3.1**:题解评论区·公告系统·站内信·个人剪贴板·题解提交开关\n- **v1.2.1**:首版正式发布(标签库、题解系统、审核流程)\n\n## 📜 许可\n\n本项目放弃继承 SYZOJ 的 [MIT License](https:\u002F\u002Fgithub.com\u002Fsyzoj\u002Fsyzoj\u002Fblob\u002Fmaster\u002FLICENSE)，目前使用 APL 2.0 开源协议。\n\n## 🙏 致谢\n\n- 原版 [SYZOJ](https:\u002F\u002Fgithub.com\u002Fsyzoj\u002Fsyzoj) 提供了完整的 OJ 基础架构\n- ZemuZzz 主导了所有定制功能的设计与实现\n- 以及所有为 Algo Beat Contest 出题组 \u002F 开发组贡献的成员\n\n---\n\nPowered by SYZOJ. Modified by **Zemu (UnratedCheater)**.\n","AlgoBeat Online Judge 是一个基于 SYZOJ 二次开发的中文在线评测与社区平台。该项目重构了 UI 框架，新增了社区互动、通知中心、关注\u002F粉丝系统、工单管理、Hit 值评分等多个功能模块。技术上采用 EJS 作为模板引擎，并实现了全本地化的静态资源加载，确保了良好的用户体验和安全性。适合需要在线编程练习、比赛组织以及编程社区建设的场景使用。",2,"2026-06-11 03:32:10","CREATED_QUERY"]