HOJ二次开发实战从修改登录动画到新增签到功能手把手教你定制自己的OJ在技术社区和编程教育领域在线判题系统OJ扮演着至关重要的角色。HOJ作为一款开源的在线判题系统因其模块化设计和良好的扩展性成为许多开发者和教育机构的首选。本文将聚焦HOJ的二次开发实战带你从零开始实现几个典型的功能定制案例。1. 前端定制修改全局加载动画HOJ默认的加载动画虽然简洁但缺乏个性。我们可以通过修改前端代码来实现自定义动画效果。首先需要定位到动画相关的代码位置# 在HOJ前端项目中加载动画通常位于以下路径 src/components/Common/Loading.vue修改这个Vue组件时可以考虑以下几种动画方案CSS动画使用keyframes实现平滑过渡效果Lottie动画导入JSON格式的复杂矢量动画SVG动画创建轻量级的矢量图形动画推荐方案使用CSS动画因为它性能最优且兼容性最好。下面是一个波纹扩散效果的实现代码template div classloading-container div classripple 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 后端API开发在Spring Boot后端项目中创建签到控制器RestController RequestMapping(/api/checkin) public class CheckInController { Autowired private UserCheckInService userCheckInService; PostMapping(/do) public CommonResultVoid doCheckIn(RequestParam(value fortune, required false) String fortune) { // 获取当前用户ID String uid UserSessionUtil.getUserInfo().getUid(); return userCheckInService.doCheckIn(uid, fortune); } GetMapping(/status) public CommonResultCheckInStatusVO getCheckInStatus() { String uid UserSessionUtil.getUserInfo().getUid(); return userCheckInService.getCheckInStatus(uid); } }2.3 前端界面实现在前端项目中创建签到组件template div classcheck-in-card h3每日签到/h3 div v-ifstatus.checked classchecked p已连续签到 {{ status.continuousDays }} 天/p p v-ifstatus.fortune今日运势{{ status.fortune }}/p /div div v-else classunchecked button clickshowFortune true立即签到/button div v-ifshowFortune classfortune-selector p求个今日运势吧/p button v-foritem in fortunes :keyitem clickdoCheckIn(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); } } } }; /script3. 实用功能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 classimport-fps h3FPS格式导入/h3 p支持导入题目描述、测试用例和strong标准答案/strong/p input typefile changehandleFileChange accept.xml / button clicksubmit开始导入/button /div /template4. 安全增强登录日志功能记录用户登录行为是保障系统安全的重要手段。我们可以为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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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 CommonResultIPageUserLoginLog 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) { QueryWrapperUserLoginLog 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;5.2 后端实现创建班级题单服务Service public class ClassProblemListServiceImpl implements ClassProblemListService { Autowired private ClassProblemListMapper listMapper; Autowired private ClassProblemListItemMapper itemMapper; Override public CommonResultVoid syncListToClass(Long listId, Long classId) { // 验证权限... // 获取题单所有题目 ListClassProblemListItem items itemMapper.selectList( new QueryWrapperClassProblemListItem().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 classclass-problem-list div classheader h3班级题单管理/h3 button clickshowCreateDialog true新建题单/button /div div classlist-container div v-forlist in lists :keylist.id classlist-item h4{{ list.name }}/h4 p{{ list.description }}/p button clicksyncList(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原有的架构设计遵循其代码规范其次修改前后端代码时要考虑兼容性避免影响现有功能最后每个新功能都应该有完善的测试用例。