news 2026/4/18 1:10:29

HOJ二次开发实战:从修改登录动画到新增签到功能,手把手教你定制自己的OJ

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HOJ二次开发实战:从修改登录动画到新增签到功能,手把手教你定制自己的OJ

HOJ二次开发实战:从修改登录动画到新增签到功能,手把手教你定制自己的OJ

在技术社区和编程教育领域,在线判题系统(OJ)扮演着至关重要的角色。HOJ作为一款开源的在线判题系统,因其模块化设计和良好的扩展性,成为许多开发者和教育机构的首选。本文将聚焦HOJ的二次开发实战,带你从零开始实现几个典型的功能定制案例。

1. 前端定制:修改全局加载动画

HOJ默认的加载动画虽然简洁,但缺乏个性。我们可以通过修改前端代码来实现自定义动画效果。首先需要定位到动画相关的代码位置:

# 在HOJ前端项目中,加载动画通常位于以下路径 src/components/Common/Loading.vue

修改这个Vue组件时,可以考虑以下几种动画方案:

  • CSS动画:使用@keyframes实现平滑过渡效果
  • Lottie动画:导入JSON格式的复杂矢量动画
  • SVG动画:创建轻量级的矢量图形动画

推荐方案:使用CSS动画,因为它性能最优且兼容性最好。下面是一个波纹扩散效果的实现代码:

<template> <div class="loading-container"> <div class="ripple"> <div></div> <div></div> </div> <p>{{ message }}</p> </div> </template> <style scoped> .ripple { position: relative; width: 64px; height: 64px; margin: 0 auto; } .ripple div { position: absolute; border: 4px solid #1890ff; opacity: 1; border-radius: 50%; animation: ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite; } .ripple div:nth-child(2) { animation-delay: -0.5s; } @keyframes ripple { 0% { top: 28px; left: 28px; width: 0; height: 0; opacity: 1; } 100% { top: -1px; left: -1px; width: 58px; height: 58px; opacity: 0; } } </style>

提示:修改完成后,记得运行npm run build重新构建前端资源,并更新Docker镜像。

2. 功能扩展:实现签到求签系统

签到功能可以增加用户粘性和平台趣味性。我们需要同时修改前端和后端代码来实现这个功能。

2.1 数据库设计

首先在HOJ的数据库中创建签到记录表:

CREATE TABLE `user_check_in` ( `id` bigint NOT NULL AUTO_INCREMENT, `uid` varchar(32) NOT NULL COMMENT '用户ID', `check_in_date` date NOT NULL COMMENT '签到日期', `fortune` varchar(255) DEFAULT NULL COMMENT '求签结果', `continuous_days` int DEFAULT '1' COMMENT '连续签到天数', PRIMARY KEY (`id`), UNIQUE KEY `uid_date` (`uid`,`check_in_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2.2 后端API开发

在Spring Boot后端项目中,创建签到控制器:

@RestController @RequestMapping("/api/checkin") public class CheckInController { @Autowired private UserCheckInService userCheckInService; @PostMapping("/do") public CommonResult<Void> doCheckIn(@RequestParam(value = "fortune", required = false) String fortune) { // 获取当前用户ID String uid = UserSessionUtil.getUserInfo().getUid(); return userCheckInService.doCheckIn(uid, fortune); } @GetMapping("/status") public CommonResult<CheckInStatusVO> getCheckInStatus() { String uid = UserSessionUtil.getUserInfo().getUid(); return userCheckInService.getCheckInStatus(uid); } }

2.3 前端界面实现

在前端项目中创建签到组件:

<template> <div class="check-in-card"> <h3>每日签到</h3> <div v-if="status.checked" class="checked"> <p>已连续签到 {{ status.continuousDays }} 天</p> <p v-if="status.fortune">今日运势:{{ status.fortune }}</p> </div> <div v-else class="unchecked"> <button @click="showFortune = true">立即签到</button> <div v-if="showFortune" class="fortune-selector"> <p>求个今日运势吧!</p> <button v-for="item in fortunes" :key="item" @click="doCheckIn(item)"> {{ item }} </button> </div> </div> </div> </template> <script> const FORTUNES = ['大吉', '中吉', '小吉', '末吉', '凶']; export default { data() { return { status: { checked: false, continuousDays: 0 }, showFortune: false, fortunes: FORTUNES }; }, methods: { async doCheckIn(fortune) { try { await this.$api.checkIn.do(fortune); this.status = await this.$api.checkIn.status(); this.showFortune = false; } catch (error) { this.$error(error); } } } }; </script>

3. 实用功能:FPS格式答案导入

对于编程教学场景,能够批量导入题目和答案可以极大提高效率。HOJ支持FPS格式的题目导入,但默认不支持答案导入,我们可以扩展这个功能。

3.1 修改FPS解析器

在后端项目中找到FPS解析器类(通常名为FpsProblemParser),添加答案解析逻辑:

public class FpsProblemParser { // ... 原有代码 ... private void parseAnswer(Problem problem, Element item) { Element answerElement = item.element("answer"); if (answerElement != null) { String answer = answerElement.getText(); problem.setAnswer(answer); } } public Problem parse(Element item, boolean isUpload) { Problem problem = new Problem(); // ... 原有解析逻辑 ... parseAnswer(problem, item); return problem; } }

3.2 更新题目服务

修改题目服务,在导入题目时保存答案:

@Service public class ProblemServiceImpl implements ProblemService { // ... 原有代码 ... @Override @Transactional(rollbackFor = Exception.class) public void addProblem(Problem problem) { // ... 原有保存逻辑 ... if (StringUtils.isNotBlank(problem.getAnswer())) { ProblemAnswer answer = new ProblemAnswer(); answer.setPid(problem.getId()); answer.setAnswer(problem.getAnswer()); problemAnswerMapper.insert(answer); } } }

3.3 前端适配

在前端导入界面添加答案导入提示:

<template> <div class="import-fps"> <h3>FPS格式导入</h3> <p>支持导入题目描述、测试用例和<strong>标准答案</strong></p> <input type="file" @change="handleFileChange" accept=".xml" /> <button @click="submit">开始导入</button> </div> </template>

4. 安全增强:登录日志功能

记录用户登录行为是保障系统安全的重要手段。我们可以为HOJ添加详细的登录日志功能。

4.1 创建登录日志表

CREATE TABLE `user_login_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `uid` varchar(32) NOT NULL COMMENT '用户ID', `login_time` datetime NOT NULL COMMENT '登录时间', `ip` varchar(45) NOT NULL COMMENT '登录IP', `user_agent` varchar(512) DEFAULT NULL COMMENT '用户代理', `location` varchar(100) DEFAULT NULL COMMENT '地理位置', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1-成功 0-失败', PRIMARY KEY (`id`), KEY `idx_uid` (`uid`), KEY `idx_time` (`login_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 实现日志记录拦截器

创建Spring拦截器记录登录行为:

public class LoginLogInterceptor implements HandlerInterceptor { @Autowired private UserLoginLogService loginLogService; @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { if (request.getRequestURI().equals("/api/login") && response.getStatus() == 200) { String username = request.getParameter("username"); UserLoginLog log = new UserLoginLog(); log.setUid(getUidByUsername(username)); log.setLoginTime(new Date()); log.setIp(IpUtils.getIpAddr(request)); log.setUserAgent(request.getHeader("User-Agent")); log.setLocation(IpUtils.getCityInfo(log.getIp())); log.setStatus(1); loginLogService.save(log); } } }

4.3 添加登录日志查询接口

@RestController @RequestMapping("/api/admin") public class AdminLoginLogController { @Autowired private UserLoginLogService loginLogService; @GetMapping("/login-logs") public CommonResult<IPage<UserLoginLog>> getLoginLogs( @RequestParam(value = "username", required = false) String username, @RequestParam(value = "ip", required = false) String ip, @RequestParam(value = "status", required = false) Integer status, @RequestParam(value = "startTime", required = false) String startTime, @RequestParam(value = "endTime", required = false) String endTime, @RequestParam(value = "current", defaultValue = "1") int current, @RequestParam(value = "size", defaultValue = "10") int size) { QueryWrapper<UserLoginLog> wrapper = new QueryWrapper<>(); // 构建查询条件... return CommonResult.success(loginLogService.page(new Page<>(current, size), wrapper)); } }

5. 教学辅助:班级同步题单功能

对于教育用途的OJ系统,能够按班级同步题单是非常实用的功能。

5.1 数据库设计

CREATE TABLE `class_problem_list` ( `id` bigint NOT NULL AUTO_INCREMENT, `class_id` bigint NOT NULL COMMENT '班级ID', `name` varchar(100) NOT NULL COMMENT '题单名称', `description` text COMMENT '题单描述', `creator` varchar(32) NOT NULL COMMENT '创建者', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_class_id` (`class_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `class_problem_list_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `list_id` bigint NOT NULL COMMENT '题单ID', `pid` bigint NOT NULL COMMENT '题目ID', `sort` int DEFAULT '0' COMMENT '排序', PRIMARY KEY (`id`), UNIQUE KEY `uk_list_problem` (`list_id`,`pid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

5.2 后端实现

创建班级题单服务:

@Service public class ClassProblemListServiceImpl implements ClassProblemListService { @Autowired private ClassProblemListMapper listMapper; @Autowired private ClassProblemListItemMapper itemMapper; @Override public CommonResult<Void> syncListToClass(Long listId, Long classId) { // 验证权限... // 获取题单所有题目 List<ClassProblemListItem> items = itemMapper.selectList( new QueryWrapper<ClassProblemListItem>().eq("list_id", listId) ); // 为班级每个成员分配这些题目 items.forEach(item -> { assignProblemToClassMembers(item.getPid(), classId); }); return CommonResult.success(); } private void assignProblemToClassMembers(Long pid, Long classId) { // 实现题目分配给班级成员的逻辑 } }

5.3 前端界面

创建班级题单管理页面:

<template> <div class="class-problem-list"> <div class="header"> <h3>班级题单管理</h3> <button @click="showCreateDialog = true">新建题单</button> </div> <div class="list-container"> <div v-for="list in lists" :key="list.id" class="list-item"> <h4>{{ list.name }}</h4> <p>{{ list.description }}</p> <button @click="syncList(list.id)">同步到班级</button> </div> </div> </div> </template> <script> export default { data() { return { lists: [], showCreateDialog: false }; }, methods: { async syncList(listId) { try { await this.$api.classProblemList.sync(listId, this.classId); this.$success('题单同步成功'); } catch (error) { this.$error(error); } } } }; </script>

在实现这些二次开发功能时,有几个关键点需要注意:首先确保理解HOJ原有的架构设计,遵循其代码规范;其次,修改前后端代码时要考虑兼容性,避免影响现有功能;最后,每个新功能都应该有完善的测试用例。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 1:08:39

把 SNC 真正接进 SAP 系统,外部安全产品、GSS-API V2 与通信链路的全景拆解

很多团队在做 SAP 安全加固时,会把注意力都放在证书、票据、Kerberos 或者 X.509 证书本身上,结果项目推进到一半,才发现真正决定成败的,不是令牌长什么样,而是 SNC 这一层有没有接对,外部安全库有没有按 GSS-API V2 的约定被正确装载,前后端是不是在用同一套可互认的实…

作者头像 李华
网站建设 2026/4/18 1:09:28

Fusuma设计模式分析:MVC架构在照片选择器中的应用

Fusuma设计模式分析&#xff1a;MVC架构在照片选择器中的应用 【免费下载链接】Fusuma Instagram-like photo browser and a camera feature with a few line of code in Swift. 项目地址: https://gitcode.com/gh_mirrors/fusu/Fusuma Fusuma是一款开源的照片选择器框架…

作者头像 李华
网站建设 2026/4/14 12:04:45

Python的__enter__和__exit__:上下文管理器的异常处理

Python的__enter__和__exit__&#xff1a;上下文管理器的异常处理 在Python中&#xff0c;上下文管理器通过__enter__和__exit__方法实现了资源的自动管理&#xff0c;例如文件操作或数据库连接。这种机制不仅简化了代码&#xff0c;还能有效处理异常&#xff0c;确保资源被正…

作者头像 李华
网站建设 2026/4/14 12:01:26

0Ω电阻最大能承受多大电流

0Ω电阻的电流承载能力是其最核心的电气参数之一&#xff0c;它并非一个固定值&#xff0c;而是由电阻的封装尺寸、额定功率、环境温度、PCB布局散热条件以及瞬时脉冲电流能力共同决定的。其本质是一个具有微小阻值&#xff08;通常为毫欧级&#xff09;和一定功率容量的特殊电…

作者头像 李华
网站建设 2026/4/14 11:59:13

2026届毕业生推荐的AI科研平台实际效果

Ai论文网站排名&#xff08;开题报告、文献综述、降aigc率、降重综合对比&#xff09; TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 就人工智能生成内容变得越发普及的这种背景状况之下&#xff0c;降AI工具适时问世了&#xf…

作者头像 李华