UOJ 官方版部署指北(2024 年)

前言

计划给学校搭建一个 OJ,选择了 vfleaking 的 Universal Online Judge。但是 UOJ 社区版安装后总是报错 ”Judgement Failed“,开了一个刚装好系统的一个 GCP 服务器也是有问题,无奈只能跑去折腾官方版。

本文所涉及的内容为 UOJ 官方版(链接见上)而不是开源社区版(UOJ-System)!
文章内容过多,建议利用左侧目录进行导航。

安装 UOJ 官方版 Docker 镜像

在这一节建议使用境外服务器或者全程挂梯子 + TUN 模式。
如果不开梯子会遇到各种问题比如 Docker 无法安装,无法拉取 ubuntu 镜像,无法连接 Node.js NPM、PHP Composer 等等……

安装 Docker

这一步需要在终端执行下面的指令:

sudo su # 切换到 root 账户环境
curl -fsSL https://get.docker.com -o get-docker.sh # 下载 Docker 安装脚本
sudo sh ./get-docker.sh --dry-run # 运行安装脚本
Bash

一般情况下,Docker Compose 将作为 Docker 的插件一并安装。

构建镜像

使用 Git 将官方版镜像克隆到本地:

git clone https://github.com/vfleaking/uoj.git
Bash

使用 cd 命令进入 uoj 目录,查看、修改 docker-compose.yml

version: '3'

services:
  all:
    build: .
    container_name: uoj_all
    restart: always
    stdin_open: true
    tty: true
    cap_add:
      - SYS_PTRACE
    volumes:
      - mysql:/var/lib/mysql
      - data_main:/var/uoj_data
      - data_copy:/var/uoj_data_copy
      - web:/opt/uoj/web
      - var_log:/var/log
      - judger:/opt/uoj/judger
      - svn:/var/svn
    ports:
     - "80:80" # UOJ 主端口,冒号左边的端口号也可以改成其他未被占用的端口
     - "888:888" # phpMyAdmin 端口,同理也可以修改
     - "3690:3690" # SVN 端口,不建议修改

volumes:
  mysql:
  data_main:
  data_copy:
  web:
  var_log:
  judger:
  svn:
YAML

然后在当前目录下执行

docker compose up
YAML
不加 -d 参数代表在前台运行。
不一开始就使用后台运行方式的原因是在第一次启动时如果出现问题查看日志可以快速定位问题。

此时 Docker 会进行 UOJ 镜像的构建,这个过程依服务器性能和网络速度大概需要 10 - 20 分钟。

当最后提示

[+] Running 2/2
 ✔ Volume "uoj_web"   Created                                        
 ✔ Container uoj_all  Created
 Attaching to uoj_all
uoj_all  |  * Starting NTP server ntpd                                   [ OK ] 
uoj_all  |  * Starting MySQL database server mysqld                      [ OK ]
......

即代表构建完成,此时按下 Ctrl+C 退出容器,再使用:

docker compose up -d
Bash

即可将 UOJ 置于后台运行。

配置 UOJ

这一步可能需要在 宿主机 和 容器 的终端之间反复切换,请注意识别。

使用以下命令进入 容器 终端:

docker attach uoj_all
Bash

在容器中执行 exit 即可返回 宿主机 终端。

修改域名、端口

在 容器 终端执行:

cd /opt/uoj
vim web/app/.config.php
Bash

进行修改:

<?php
return [
	'database' => [
		'database'  => 'app_uoj233',
		'username' => 'root',
		'password' => 'xxxxxxxxxxxxxxxxxxx', # 这里应该已经生成了默认密码,记录下来
		'host' => '127.0.0.1'
	],
	'web' => [
		'main' => [
			'protocol' => 'http', # HTTP 协议,可选 http / https
			'host' => 'local_uoj.ac', # 域名
			'port' => 80 # 端口号,如果想要网址隐藏端口号的话 http 使用 80,https 使用 443
		],
		'blog' => [
			'protocol' => 'http',
			'host' => 'blog.local_uoj.ac',
			'port' => 80
		],
		'domain' => null
	],
	'security' => [
		'user' => [
			'client_salt' => 'salt0'
		],
		'cookie' => [
			'checksum_salt' => ['salt1', 'salt2', 'salt3']
......
PHP

这时可以通过 域名:端口号 的方式尝试访问 UOJ,如果显示正常则证明没有问题。

注册账号

在 UOJ 上注册 root 账号,注册的第一个账号将自动赋予超级管理员。

安装 phpMyAdmin

phpMyAdmin 将用于管理 UOJ 上的 MySQL 数据库。

在 容器 终端上执行:

cd /var/www
wget https://files.phpmyadmin.net/phpMyAdmin/5.2.1/phpMyAdmin-5.2.1-all-languages.zip # 下载
unzip phpMyAdmin-5.2.1-all-languages.zip -d ./ # 解压
mv ./phpMyAdmin-5.2.1-all-languages ./phpMyAdmin # 重命名
Bash

随后转到 /etc/apache2/sites-available 文件夹,创建 001-phpmy.conf

<VirutalHost *:888>
    DocumentRoot /var/www/phpMyAdmin
</VirutalHost>
XML

然后编辑 /etc/apache2/ports.conf,在 Listen 80 后面新建一行插入:

Listen 888
Plaintext

建立链接并重启 Apache2:

ln -s /etc/apache2/sites-available/001-phpmy.conf /etc/apache2/sites-enabled
service apache2 restart
Bash

切回到 /var/www/phpMyAdmin 目录,运行以下指令创建并编辑 phpMyAdmin 默认配置:

cp config.sample.inc.php config.inc.php
vim config.inc.php
Bash

进行编辑:

/**
 * First server
 */
$i++;
/* Authentication type */
$cfg['Servers'][$i]['auth_type'] = 'cookie';
/* Server parameters */
$cfg['Servers'][$i]['host'] = '127.0.0.1'; // 修改
$cfg['Servers'][$i]['compress'] = false;
$cfg['Servers'][$i]['AllowNoPassword'] = false;

/**
 * phpMyAdmin configuration storage settings.
 */

/* User used to manipulate with storage */
PHP

随后执行 mysql --password app_uoj233 进入 MySQL 客户端进行用户表的修改:

Enter password: <数据库密码>
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 328
Server version: 8.0.39-0ubuntu0.20.04.1 (Ubuntu)

Copyright (c) 2000, 2024, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> UPDATE mysql.user SET host = '%' WHERE user = 'root';
Query OK, 1 row affected (0.07 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.09 sec)

mysql> quit
Bye
Plaintext

这时,你应当能通过之前在 docker-compose 中设定的对外端口访问到 phpMyAdmin 并进行登录了。

修改博客为单域名模式

下内容基于 Initial commit for PHP7 · UniversalOJ/UOJ-System@babd303 (github.com)
UOJ 官方版和社区版差异已经比较大,因此链接内容可能如今不适合官方版 UOJ,仅供参考。
行号可能会有改变,善用查找功能。

在 容器 内目录 /opt/uoj/web 下修改下面这些文件:

【覆盖】app/controllers/subdomain/blog/route.php

修改博客路由

<?php

call_user_func(function () {
        Route::pattern('blog_username', '[a-zA-Z0-9_\-]{1,20}');

        $prefix = '/blogof/{blog_username}';

        Route::group([
                        'domain' => UOJConfig::$data['web']['main']['host'],
                        'protocol' => UOJConfig::$data['web']['main']['protocol'],
                        'onload' => function() {
                                UOJContext::setupBlog();
                        }
                ], function() use ($prefix) {
                        Route::any("$prefix/", '/subdomain/blog/index.php');
                        Route::any("$prefix/archive", '/subdomain/blog/archive.php');
                        Route::any("$prefix/aboutme", '/subdomain/blog/aboutme.php');
                        Route::any("$prefix/click-zan", '/click_zan.php');
                        Route::any("$prefix/blog/{id}", '/subdomain/blog/blog.php');
                        Route::any("$prefix/slide/{id}", '/subdomain/blog/slide.php');
                        Route::any("$prefix/blog/(?:{id}|new)/write", '/subdomain/blog/blog_write.php?type=B');
                        Route::any("$prefix/slide/(?:{id}|new)/write", '/subdomain/blog/blog_write.php?type=S');
                        Route::any("$prefix/blog/{id}/delete", '/subdomain/blog/blog_delete.php');
                        Route::any("$prefix/blog/{id}/content.md", '/download.php?type=blog-md');
                        Route::any("$prefix/slide/{id}/content.yaml", '/download.php?type=slide-yaml');
                }
        );
});
PHP

【修改】app/models/Route.php

加几个 rtrim

        protected static function addRoute($methods, $uri, $action) {
                if (is_string($methods)) {
                        $methods = [$methods];
                }

                $cur = [];
                $cur['methods'] = $methods;
                $cur['uri'] = rtrim($uri, '/');
                if($cur['uri'] == '') {
                        $cur['uri'] = "/";
                }
                $cur['action'] = $action;
                $cur = array_merge(self::getGroup(), $cur);
                self::$routes[] = $cur;
                return $cur;
        }
PHP
                if (isset($route['domain'])) {
                        $domain_pat = strtr($route['domain'], $rep_arr);
                        if (!preg_match('/^'.$domain_pat.'$/', rtrim(UOJContext::requestDomain(), '/'), $domain_matches)) {
                                return false;
                        }
                        $matches = array_merge($matches, $domain_matches);
                }
PHP

【修改】app/models/HTML.php

前端链接

        public static function blog_url($username, $uri, array $cfg = []) {
                $cfg += [
                        'escape' => true
                ];

                $protocol = HTML::protocol('main');
                $url = UOJConfig::$data['web']['main']['protocol'].'://'.UOJConfig::$data['web']['main']['host'].$port.'/blogof/'.blog_name_encode($username);
                if (HTML::port('blog') != HTML::standard_port($protocol)) {
                        $url .= ':'.HTML::port('blog');
                }
                $url .= $uri;
                $url = rtrim($url, '/');

                if ($cfg['escape']) {
                        $url = HTML::escape($url);
                }
                return $url;
        }
PHP

【修改】app/controllers/subdomain/blog/archive.php

按钮

<?php echoUOJPageHeader('日志') ?>

<div class="row">
        <div class="col-md-3">
                <?php if (UOJUserBlog::userCanManage(Auth::user())): ?>
                <div class="btn-group btn-group-justified">
                        <a href="<?=HTML::blog_url(UOJUserBlog::id(), '/blog/new/write')?>" class="btn btn-primary"><span class="glyphicon glyphicon-edit"></span> 写新博客</a>
                        <a href="<?=HTML::blog_url(UOJUserBlog::id(), '/slide/new/write')?>" class="btn btn-primary"><span class="glyphicon glyphicon-edit"></span> 写新幻灯片</a>
                </div>
                <?php endif ?>
PHP

【修改】app/controllers/blogs.php

还是按钮

<?php echoUOJPageHeader(UOJLocale::get('blogs')) ?>

<?php if (Auth::check()): ?>
<div class="pull-right">
        <a href="<?= HTML::blog_url(Auth::id(), '/') ?>" class="btn btn-info btn-sm">我的博客首页</a>
        <a href="<?= HTML::blog_url(Auth::id(), '/blog/new/write') ?>" class="btn btn-info btn-sm">写新博客</a>
</div>
<?php endif ?>
<h3>博客总览</h3>
<?php
    echoLongTable(
PHP

【修改】app/views/blog-nav.php

导航栏链接

                                <span class="icon-bar"></span>
                                <span class="icon-bar"></span>
                        </button>
                        <a class="navbar-brand" href="<?= HTML::blog_url(UOJUserBlog::id(), '/') ?>"><?= UOJUserBlog::id() ?></a>
                </div>
                <div class="navbar-collapse collapse">
                        <ul class="nav navbar-nav">
                                <li><a href="<?= HTML::blog_url(UOJUserBlog::id(), '/archive') ?>">日志</a></li>
                                <li><a href="<?= HTML::blog_url(UOJUserBlog::id(), '/aboutme') ?>">关于我</a></li>
                                <li><a href="<?= HTML::url('/') ?>">DYYZOJ</a></li>
                        </ul>
                </div><!--/.nav-collapse -->
        </div>
</div>
PHP

【覆盖】app/views/blog-preview.php

预览界面,修改的太多了,直接覆盖吧

<?php
        if ($is_preview) {
                $readmore_pos = strpos($blog->content['content'], '<!-- readmore -->');
                if ($readmore_pos !== false) {
                        $content = substr($blog->content['content'], 0, $readmore_pos).'<p><a href="'.HTML::blog_url(UOJUserBlog::id(), '/blog/').info['id'].'">阅读更多……</a></p>';
                } else {
                        $content = $blog->content['content'];
                }
        } else {
                $content = $blog->content['content'];
        }

        $extra_text = $blog->info['is_hidden'] ? '<span class="text-muted">[已隐藏]</span> ' : '';

        $blog_type = $blog->info['type'] == 'B' ? 'blog' : 'slide';
?>
<h2><?= $extra_text ?><a class="header-a" href="<?= HTML::blog_url(UOJUserBlog::id(), '/blog/'.$blog['id']) ?>"><?= $blog->info['title'] ?></a></h2>
<div><?= $blog->info['post_time'] ?> <strong>By</strong> <?= getUserLink($blog->info['poster']) ?></div>
<?php if (!$show_title_only): ?>
<div class="panel panel-default">
        <div class="panel-body">
                <?php if ($blog->isTypeB()): ?>
                <article class="uoj-article"><?= $content ?></article>
                <?php elseif ($blog->isTypeS()): ?>
                <article class="uoj-article">
                        <div class="embed-responsive embed-responsive-16by9">
                                <iframe class="embed-responsive-item" src="<?= HTML::blog_url(UOJUserBlog::id(), '/slide/'.$blog['id']) ?>"></iframe>
                        </div>
                        <div class="text-right top-buffer-sm">
                                <a class="btn btn-default btn-md" href="<?= HTML::blog_url(UOJUserBlog::id(), '/slide/'.$blog['id']) ?>"><span class="glyphicon glyphicon-fullscreen"></span> 全屏</a>
                        </div>
                </article>
                <?php endif ?>
        </div>
        <div class="panel-footer text-right">
                <ul class="list-inline bot-buffer-no">
                        <li>
                        <?php foreach ($blog->tags as $tag): ?>
                                <?php echoBlogTag($tag) ?>
                        <?php endforeach ?>
                        </li>
                        <?php if ($is_preview): ?>
                        <li><a href="<?= HTML::blog_url(UOJUserBlog::id(), '/blog/'.$blog['id']) ?>">阅读全文</a></li>
                        <?php endif ?>
                        <?php if (Auth::check() && (isSuperUser(Auth::user()) || Auth::id() == $blog->info['poster'])): ?>
                        <li><a href="<?=HTML::blog_url(UOJUserBlog::id(), '/'.$blog_type.'/'.$blog['id'].'/write')?>">修改</a></li>
                        <li><a href="<?=HTML::blog_url(UOJUserBlog::id(), '/blog/'.$blog['id'].'/delete')?>">删除</a></li>
                        <?php endif ?>
                        <li><?= ClickZans::getBlock('B', $blog->info['id'], $blog->info['zan']) ?></li>
                </ul>
        </div>
</div>
<?php endif ?>
PHP

导出镜像

刚才我们所做的所有修改都只是在容器生命周期内生效,一旦停止容器即丢弃,因此我们需要导出自定义镜像。

在 宿主机 终端上运行:

docker commit uoj_all 新镜像名字
Bash

后面即可使用 docker run 运行新镜像。

还可以推送到阿里云私有仓库(个人免费):https://help.aliyun.com/zh/ack/create-an-application-by-using-a-private-image-repository

此作品(UOJ 官方版部署指北(2024 年))基于 CC-BY-NC-SA 4.0 协议授权。

转载请注明来源:作者:CodeZhangBorui,链接:https://codezhangborui.com/2024/08/uoj-official-edition-deploy-guide/

暂无评论

发送评论 编辑评论


				
上一篇
下一篇