部署SCNUOJ

前置条件:服务器为2核4G内存60G硬盘5M带宽的腾讯云,ubuntu20.04,刚租完开机的初始状态

SCNUOJ代码仓

安装

拉仓库

腾讯云默认登录是 ubuntu 用户,先提个权:

1
sudo su

root 下才能操作并新建这个 /var/www/scnuoj 目录,以该目录为项目根目录。

因为下文 git clone 会新建文件夹,所以实际上在 /var/www 执行下面 clone 即可

在项目根目录下,克隆仓库:(仓库大小约 ,且可能腾讯云限速问题,故需要一定时间)

1
git clone https://github.com/scnu-socoding/scnuoj.git

(https 不行,会 GnuTLS recv error (-54): Error in the pull function.)

git 协议不行,如果本地 git 没有配置用户的话,所以推荐 https 协议

新机子没有密钥,不能 clone,可以生成一个,如:

1
ssh-keygen -t rsa -C "lr580@scnu.edu.cn" #一路回车

装依赖

腾讯云初始无 composer,需要先:

1
apt install composer

composer install 会安装当前目录下的 composer.json 的内容,用于管理 PHP 依赖

看看自己装没装 PHP 或核对版本,使用 php -v,查看 composer 需要的版本:

1
composer show --platform

使用 root 用户,执行:(虽然 root 会提示 Do not run Composer as root/super user!,但是这个目录只有 root 有权限)

1
composer install

如果遇到了:

1
2
the requested PHP extension curl is missing from your system
the requested PHP extension dom is missing from your system

安装 php 扩展 ext-domext-curl,如:(非 root 则 sudo)

1
2
apt-get install php7.4-dom
apt-get install php-curl

当前还没启动服务器,如果启动了,要重启 apache 或 nginx

如果一直卡 connecting,可以连镜像站:

1
2
3
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
#取消镜像站:
composer config -g --unset repos.packagist

以 font awesome 为例,如果镜像也不行,可以手动下载:

如在 release 找到对应版本(卡connecting的版本),下载 source code,发给服务器。(我在做这个的时候,connecting 转龟速 download 了,所以暂时不管这个了,这个没做下去)

数据库

LNMP 其实本来应该 composer 前做的,但是我盲了

LNMP Linux + NGINX + MariaDB + PHP(7.4 或更高版本)

需要到的 PHP 插件包括 php-curl, php-xml, php-fpm, php-mysql。除去上面装了的,还需要安装:

1
2
3
apt-get install php-xml
apt-get install php-fpm
apt-get install php-mysql

查看有没有装数据库:

1
mysql --version

发现没有,证明 MySQL 或 MariaDB 都没装

安装:

1
apt-get install mariadb-server

root 下执行 mysql 进入数据库,给数据库添加管理员帐号:(账号密码自取,这里任意)

1
2
3
create user 'lr580'@'localhost' identified by '1437abcd';
grant all privileges on *.* to 'lr580'@'localhost' with grant option;
flush privileges;

新建个数据库:

1
create database scnuoj;

编辑项目根目录 config/db.php,修改 username, password 和对应 dsn,分别是数据库用户名、密码、数据库名,改这几行:

1
2
3
'dsn' => 'mysql:host=localhost;dbname=scnuoj',
'username' => 'lr580',
'password' => '1437abcd',

nginx

nginx -v 发现没装 nginx。

安装 nginx:

1
apt-get install nginx

修改 /etc/nginx/sites-enabled/default,为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80;
listen [::]:80;

root /var/www/scnuoj/web;
client_max_body_size 0;
index index.php;
server_name scnuoj;

location / {
try_files $uri $uri/ /index.php?$args;
}

location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
}
}

解读:

  • 第二行表示 IPv4 端口,第三行是 IPv6 端口。

  • 在项目根目录 web/ 是 PHP 页面所在,故 /var/www/scnuoj/web 对应那个 PHP 服务器根目录

  • client_max_body_size 不限制客户端请求的请求体大小。通常,这个设置用来控制上传文件的大小

  • 默认索引网页和服务器名配置

  • 将根目录 / 的请求尝试查找匹配的文件或目录,找不到给 index

  • .php 结尾的,特殊处理,连接到套接字

    shell 输入下面指令看看那个 pass 是不是存在:

    1
    2
    3
    4
    5
    if stat /var/run/php/php7.4-fpm.sock &> /dev/null; then
    echo "Unix socket exists."
    else
    echo "Unix socket does not exist."
    fi

    如果上面 LNMP 装了那个插件,应该是 exists 的。

看看 ngnix 是否启动、是否开机自启:

1
2
systemctl is-active nginx
systemctl is-enabled nginx

重启 nginx:

1
2
nginx -t #检查配置语法错误
systemctl restart nginx

启动后,任意客户端尝试访问 http://114.132.159.213/,发现 500,能访问。证明腾讯云默认开了防火墙。

yii

在项目根目录执行:

1
./yii install

判题配置

sh 命令执行:

1
useradd -m -u 1536 judge

-m 是创建家目录,-u 制定 UID。

judge/ 下执行:

1
make

执行 make 失败,报错为:

1
2
3
4
5
6
gcc -std=gnu99 -Wall -c -I/usr/local/mysql/include/mysql -I/usr/include/mysql  src/dispatcher.c
src/dispatcher.c:33:10: fatal error: mysql/mysql.h: No such file or directory
33 | #include <mysql/mysql.h>
| ^~~~~~~~~~~~~~~
compilation terminated.
make: *** [makefile:2: all] Error 1

执行命令:

1
apt-get install libmysqlclient-dev

之后再执行,成功,一堆 warning,暂时不管。

启动判题机:

1
./dispather

关闭判题机:

1
pkill -9 dispatcher

以 OI 模式启动判题机:

1
./dispather -o

Polygon 判题机部署,在项目 polygon/ 目录:

1
make

启动出题系统判题机:

1
./polygon

数据库初始化

在 nginx 后,500 了,但是没管,现在开始修复。

开启 DEBUG,在 web/inedx.php,注释第四第五行的 defined。

发现无 scnuoj 数据库。上一步没新建,所以数据库那里新建一下。

下一个问题是数据库的数据表不存在。

升级指引 灵感启发,考虑数据库迁移当初始化,执行:

1
./yii migrate

之后需要输入 yes,然后输入账号密码和邮箱,是登录该 OJ 的管理员账号基本信息。

重启判题机。至此,部署完毕,访问网页正常。

安装编程语言

然而,发现只有 C 和 python 能过题,C++ 和 java 并不能过题。继续检查:

1
2
3
4
gcc --version
g++ --version
java --version # javac --version
python --version # python3 --version

发现没有安装 c++ 和 java,进行安装:

1
2
apt install g++
apt install default-jdk

安装结束,重新判题,都能过题,其中 C++ 版本为 201402。

验证码支持

继续测试,发现无法注册新用户。报错为:

1
2
Invalid Configuration – yii\base\InvalidConfigException
Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required.

查找 PHP 配置文件位置:

1
php -i | grep 'Loaded Configuration File'

找不到 extension=gdextension=imagick 扩展,故安装 PHP 扩展:

1
2
3
apt-get install php-gd
apt-get install imagemagick

两个图像处理库,能够支持生成图片验证码。

执行后,可以正常注册新用户。

下载测试点支持

无法下载测试点,也无法根据输入生成输出。

提示: 服务器未启用 php-zip 扩展,如需下载测试数据,请安装 php-zip 扩展。

1
sudo apt install php-zip

看到后台最大上传文件大小 8M,单个文件 2M,想改一下。

找到 PIP 路径,如输出 php -ini 看,或者直接盲猜 /etc/php/7.4/fpm/php.ini

可以 ps aux | grep php 找。

PHP CLI (Command Line Interface) 改这个没用

PHP-FPM (FastCGI Process Manager) 这个才是跑 OJ 的

修改:(第一个单文件,第二个总)

1
2
upload_max_filesize = 100M
post_max_size = 200M

vscode remote ssh 改不了可以先 chmod 改完再弄回去

重启:(非 root 不 sudo 可能要登录密码)

1
2
sudo systemctl restart php7.4-fpm
sudo systemctl restart nginx

如果改的是 cli 路径下的配置文件,则后台不见效。

二次开发SCNUOJ

学号前端修改

前端、注册把昵称改成真实姓名,学号改成学校班级,先查查有什么文件:

1
grep -rnl "学号" .

r 递归,n 显示行号,l 显示文件路径

找到有:

1
2
3
4
5
6
./modules/admin/views/user/index.php
./modules/admin/views/user/_search.php
./messages/zh-CN/app.php
./views/site/signup.php
./views/contest/_standing_group.php
./views/user/_default.php

全部 ctrl+f 改即可。

昵称同理。修改:

1
2
3
4
5
6
7
8
controllers/GroupController.php
modules/admin/models/GenerateUserForm.php
modules/admin/views/contest/clarify.php
modules/admin/views/user/_search.php
messages/zh-CN/app.php
models/User.php
views/contest/_standing_group.php
views/user/_default.php

发现问题,注册时输入学号只能输入数字,无法输入任意字符。

修改:models/SignupForm.php,注释掉 rules() 里边的:

1
['studentNumber', 'integer'],

关闭评论区

偷懒,考虑虚假关闭:只不显示前端进去的入口,但是直接 url 可以访问,不真的控制权限。

根据主页提示词,找到讨论区主页:

1
grep -r "仅展示最近三十则讨论消息,在问题页可以查看该 问题下的所有讨论。" 

发现是:

1
views/discuss/index.php

开 F12,看讨论标签,发现是 fa-comment

可以把这行字关掉。如改成:

1
<i class="fas fa-fw fa-info-circle"></i> 论坛功能不予开放,不便之处敬请谅解。

阅读 view/problem/view.php,发现有 Yii::$app->setting->get('isDiscuss')

继续搜索:grep -r isDiscuss,找到有效的:

1
modules/admin/views/setting/index.php

——原来,本来就有这个功能,后台直接选择即可。没事了。

备案号添加

域名证书可以在腾讯云直接点击容易搜到下载,如果是腾讯云买的域名

需要添加两个东西,理论上有内容就行,没有样式要求规定。这里懒,所以只渲染出内容即可。

找到主页为 views/site/index.php,可以在最底下追加:

1
2
3
4
5
<footer>
<div style="text-align: center">
<a href="https://beian.miit.gov.cn/" target="_blank" style="color:black">粤ICP备2023133862号</a>
</div>
</footer>

其中,备案号,可以在 href 里搜,如直接搜 young-oj.cn,就出来了。

相关文件:here1 here2

之后再更新

修改代码结果页

很多人反馈说“标准输出”与“答案”有歧义,

通过 grep -r "标准输出" ."答案",找到修改页为 var/www/scnuoj/web/js/app.js 的第二个搜索项(或全部),修改即可。

交 pr。

排行榜缓存优化

加载排行榜很慢需要重新计算,考虑缓存优化。下下策:隐藏接口。

中策:修改数据库结构,加表。

据说有现成实现,可以调用 Yii 的缓存功能,如:参考文献

1
2
$cache = Yii::$app->cache;
$tags = $cache->get('problem-tags');
1
2
3
4
5
6
7
8
9
10
11
12
13
public function getExpensiveQueryResult()
{
$cacheKey = 'expensive-query-result';

$data = Yii::$app->cache->get($cacheKey);
if ($data === false) {
// 缓存中没有数据,执行数据库查询
$data = $this->executeExpensiveQuery();
// 将查询结果存储到缓存中,设置有效期为 600 秒(10分钟)
Yii::$app->cache->set($cacheKey, $data, 600);
}
return $data;
}

顾名思义找到控制器为 controllers/RatingController.php,视图为 views/rating/index.php

GPT 给出对 actionIndex() 的修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public function actionIndex()
{
if (Yii::$app->setting->get('isContestMode') && (Yii::$app->user->isGuest || (!Yii::$app->user->identity->isAdmin()))) {
throw new ForbiddenHttpException('You are not allowed to perform this action.');
}

// 缓存键
$cacheKey = 'rating-index-data';
// 尝试从缓存中获取数据
$data = Yii::$app->cache->get($cacheKey);

if ($data === false) {
// 缓存中没有数据,执行数据库查询
$query = (new Query())->select('u.id, u.nickname, p.student_number, u.rating, s.solved')
->from('{{%user}} AS u')
->leftJoin(
'(SELECT COUNT(DISTINCT problem_id) AS solved, created_by FROM {{%solution}} WHERE result=4 GROUP BY created_by ORDER BY solved DESC) as s',
'u.id=s.created_by'
)
->leftJoin('`user_profile` `p` ON `p`.`user_id`=`u`.`id`')
->orderBy('solved DESC, id');
$top3users = $query->limit(3)->all();
$defaultPageSize = 50;
$countQuery = clone $query;
$pages = new Pagination([
'totalCount' => $countQuery->count(),
'defaultPageSize' => $defaultPageSize
]);
$users = $query->offset($pages->offset)
->limit($pages->limit)
->all();

// 将查询结果保存到缓存中,设置有效期为 600 秒(10分钟)
$data = [
'top3users' => $top3users,
'users' => $users,
'pages' => $pages,
'currentPage' => $pages->page,
'defaultPageSize' => $defaultPageSize
];
Yii::$app->cache->set($cacheKey, $data, 600);
}

// 使用缓存或新查询的数据渲染视图
return $this->render('index', $data);
}

添加显示上次更新的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...之前的代码

// 将查询结果和当前时间保存到缓存中
$currentTimestamp = time();
$data = [
'top3users' => $top3users,
'users' => $users,
'pages' => $pages,
'currentPage' => $pages->page,
'defaultPageSize' => $defaultPageSize,
'lastUpdated' => $currentTimestamp // 添加这一行
];
Yii::$app->cache->set($cacheKey, $data, 600);

// ...之后的代码

视图合适地方添加:

1
2
3
<?php if (isset($lastUpdated)): ?>
<p>榜单更新时间: <?= Yii::$app->formatter->asDatetime($lastUpdated) ?></p>
<?php endif; ?>

调试用,马上刷新缓存:

1
2
3
4
5
6
7
8
9
public function actionClearCache()
{
if (!Yii::$app->user->identity->isAdmin()) {
throw new ForbiddenHttpException('You are not allowed to perform this action.');
}
Yii::$app->cache->delete('rating-index-data');
Yii::$app->session->setFlash('success', 'Cache cleared successfully.');
return $this->redirect(['index']); // 重定向回主页面
}
1
<?= Html::a('刷新榜单', ['rating/clear-cache'], ['class' => 'btn btn-warning']) ?>

合并前端样式以美观化,按钮管理员可见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<style>
.btn-custom {
font-size: 0.8rem; /* 调整字体大小 */
padding: .335rem .65rem; /* 调整内边距以适应新的字体大小 */
}
</style>
<div class="row">
<div class="col">
<?php if (isset($lastUpdated)): ?>
<p style="display: inline-block; margin-right: 10px;">榜单更新时间: <?= Yii::$app->formatter->asDatetime($lastUpdated) ?></p>
<?php endif; ?>
<?php if (!Yii::$app->user->isGuest && Yii::$app->user->identity->isAdmin()): ?>
<?= Html::a('刷新榜单', ['rating/clear-cache'], ['class' => 'btn btn-warning btn-custom', 'style' => 'display: inline-block; vertical-align: middle;']) ?>
<?php endif; ?>
</div>
</div>

因为没有报错,所以 OJ 本来就支持 Yii cache 功能,不需要额外配置。pr。

后来,发现无法翻页,翻页后仍在第一页。修改前没这个 bug。考虑修复。

问题出在 offset 存缓存了,修改一下逻辑。修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public function actionIndex()
{
if (Yii::$app->setting->get('isContestMode') && (Yii::$app->user->isGuest || (!Yii::$app->user->identity->isAdmin()))) {
throw new ForbiddenHttpException('You are not allowed to perform this action.');
}

// 缓存键
$cacheKey = 'rating-index-data';
// 尝试从缓存中获取数据
$data = Yii::$app->cache->get($cacheKey);

if ($data === false) {
// 缓存中没有数据,执行数据库查询
$query = (new Query())->select('u.id, u.nickname, p.student_number, u.rating, s.solved')
->from('{{%user}} AS u')
->leftJoin(
'(SELECT COUNT(DISTINCT problem_id) AS solved, created_by FROM {{%solution}} WHERE result=4 GROUP BY created_by ORDER BY solved DESC) as s',
'u.id=s.created_by'
)
->leftJoin('`user_profile` `p` ON `p`.`user_id`=`u`.`id`')
->orderBy('solved DESC, id');
// 修改此处以获取所有用户数据
$allUsers = $query->all();

// 将查询结果和当前时间保存到缓存中
$currentTimestamp = time();
// 缓存全体数据,设置有效期为 600 秒(10分钟)
$data = [
'allUsers' => $allUsers,
'lastUpdated' => $currentTimestamp
];
Yii::$app->cache->set($cacheKey, $data, 600);
}

// 分页处理
// 假设从前端获取当前页码(pageNum)和每页大小(pageSize)
$defaultPageSize = 50;
$pages = new Pagination([
'totalCount' => count($data['allUsers']),
'defaultPageSize' => $defaultPageSize
]);
$pageNum = Yii::$app->request->get('page', 1);
$pageSize = Yii::$app->request->get('pageSize', $defaultPageSize);
$offset = ($pageNum - 1) * $pageSize;
$pagedData = array_slice($data['allUsers'], $offset, $pageSize);

// 使用缓存或新查询的数据渲染视图
// 向视图传递分页数据和其他必要信息
return $this->render('index', [
'users' => $pagedData,
'currentPage' => $pageNum,
'defaultPageSize' => $defaultPageSize,
'pages' => $pages,
'totalUsers' => count($data['allUsers']),
'lastUpdated' => $data['lastUpdated']
]);
}

重新 pr。

后来发现显示的排名不正确,多了一个页的偏移,故修改视图的:

1
<?php $num = $k + $currentPage * $defaultPageSize + 1; ?>

为:

1
<?php $num = $k + ($currentPage-1) * $defaultPageSize + 1; ?>

再次 pr。

注册时输入昵称

找到 views/site/signup.php,在 <?php $form = ActiveForm::begin(['id' => 'form-signup']); ?> 下合适位置添加:

1
2
3
4
5
6
7
8
<small class="text-secondary">请在这里填写你的昵称。若缺省则与用户名一致。</small>
<?= $form->field($model, 'nickname', [
'template' => '<div class="input-group">{input}</div>{error}',
'inputOptions' => [
'placeholder' => $model->getAttributeLabel('nickname'),
],
])->label(false);
?>

找到 modes/SignupForm.php,在 signup 将:

1
$user->nickname = $this->username;

改为:

1
2
3
4
$user->nickname = $this->nickname;
if(empty($user->nickname)) {
$user->nickname = $user->username;
}

rules() 函数里添加:

1
['nickname', 'string', 'max' => 16, 'message' => '长度不超过 16 字符'],

阅读 models/User.phpattributeLabels,它是上文 placeholder 的填充物,发现需要设置 Yii::t(messages/zh-CN/app.php)。所以依葫芦画瓢,在 models/SignupForm.php 的对应方法 attributeLabels 添加:

1
'nickname' => Yii::t('app', 'Nickname'),

model/SignupForm.php class 定义添加成员属性:

1
public $nickname;

最后更新: 2024年01月22日 16:36

原始链接: https://lr580.github.io/2023/09/02/SCNUOJ%E9%83%A8%E7%BD%B2/