class GamesController < ApplicationController before_action :require_login, :check_auth before_action :find_game, except: [:jupyter] before_action :find_shixun, only: [:show, :answer, :rep_content, :choose_build, :game_build, :game_status] before_action :allowed, except: [:jupyter] #require 'iconv' include GamesHelper include ApplicationHelper def show uid_logger("--games show start") # 防止评测中途ajaxE被取消;3改成0是为了处理首次进入下一关的问题 update_game_parameter(@game) game_challenge = Challenge.base_attrs.find(@game.challenge_id) # 选择题类型的实训关卡总分 @st = game_challenge.st if @st == 1 game_challenge.score = game_challenge.choose_score.to_i end game_count = Game.where(myshixun_id: @game.myshixun_id).count discusses = @shixun.discusses discusses = discusses.where('hidden = false OR user_id = :user_id', user_id: current_user.id) unless current_user.admin_or_business? discusses_count = discusses.count @user = @game.owner is_teacher = @user.is_teacher? # 实训超时设置 time_limit = game_challenge.exec_time # 上一关、下一关 prev_game = @game.prev_of_current_game(@shixun.id, @game.myshixun_id, game_challenge.position) #next_game = @game.next_of_current_game(@shixun.id, @game.myshixun_id, game_challenge.position) next_game = user_next_game(@shixun, game_challenge, @game, @identity) # 关卡点赞数, praise_or_tread = 1则表示赞过 praise_count = game_challenge.praises_count user_praise = game_challenge.praise_treads.exists?(user_id:current_user.id, praise_or_tread: 1) # 实训的最大评测次数,这个值是为了优化查询,每次只取最新的最新一次评测的结果集 max_query_index = @game.query_index.to_i # 统计评测时间 record_onsume_time = EvaluateRecord.where(game_id: @game.id).first.try(:pod_execute) # myshixun_manager判断用户是否有权限查看隐藏测试集(TPM管理员;平台认证的老师;花费金币查看者) myshixun_manager = @identity < User::EDU_GAME_MANAGER # 选择题和编程题公共部分 @base_date = {st: @st, discusses_count: discusses_count, game_count: game_count, myshixun: @myshixun, challenge: game_challenge.attributes.except("answer"), game: @game.try(:attributes), shixun: @shixun.attributes.except("vnc", "vnc_evaluate"), record_onsume_time: record_onsume_time, prev_game: prev_game, next_game: next_game, praise_count: praise_count, user_praise: user_praise, time_limit: time_limit, tomcat_url: edu_setting('cloud_tomcat_php'), is_teacher: is_teacher, myshixun_manager: myshixun_manager, git_url: (@shixun.vnc ? repo_url(@myshixun.repo_path) : "")} if @shixun.vnc get_vnc_link(@game) end # 区分选择题和编程题,st:0编程题; if @st == 0 has_answer = game_challenge.challenge_answers.size != 0 game_challenge.answer = nil mirror_name = @shixun.mirror_name # 判断tpm是否修改了 begin tpm_modified = @myshixun.repository_is_modified(@shixun.repo_path) # 判断TPM和TPI的版本库是否被改了 rescue uid_logger("实训平台繁忙,繁忙等级(81)") end tpm_cases_modified = (game_challenge.modify_time != @game.modify_time) # modify_time 决定TPM测试集是否有更新 @task_result = {tpm_modified: tpm_modified, tpm_cases_modified: tpm_cases_modified, mirror_name: mirror_name, has_answer: has_answer} testset_detail max_query_index, game_challenge else # 选择题类型的 # 该方法多个地方调用,比如show、评测 # 最后一个字段true表示只显示数据,false表示可能有添加数据 choose_container(game_challenge, @game, max_query_index) end end def jupyter # Jupyter没有challenge @myshixun = Myshixun.find_by_identifier params[:identifier] unless current_user.id == @myshixun.user_id || current_user.admin_or_business? raise Educoder::TipException.new(403, "..") end @shixun = @myshixun.shixun # 判断tpm是否修改了 begin @tpm_modified = @myshixun.repository_is_modified(@shixun.repo_path) # 判断TPM和TPI的版本库是否被改了 rescue uid_logger("服务器出现问题,请重置刷新页面") end end def reset_vnc_link begin # 删除vnc的pod delete_vnc(@game) # 重新连接 get_vnc_link(@game) render :json => {status: 1, message: "重置VNC成功", data: {vnc_url: @vnc_url, vnc_evaluate: @vnc_evaluate}} rescue Exception => e logger.error("############'#{e.message}'") tip_exception("实训云平台繁忙") end end # 查看效果 # todo : 这块代码有很大的改进空间 # todo : 中文排序问题 def picture_display myshixun = Myshixun.find(@game.myshixun_id) if myshixun.main_mirror.try(:type_name) == "Android" @type = "qrcode" workspace = @game.try(:picture_path) game_challenge = @game.challenge qr = RQRCode::QRCode.new("#{edu_setting('host_name')}/api/shixuns/download_file?file_name=#{workspace}/#{game_challenge.picture_path}/manual-ok.apk", :size => 12, :level => :h) @qrcode_str = Base64.encode64( qr.to_img.resize(400,400).to_s ) else #conv = Iconv.new("GBK", "utf-8") @game_challenge = @game.challenge type = @game_challenge.show_type @type = shixun_show_type type workspace_path = @game.try(:picture_path) @answer_path = "#{Rails.root}/#{workspace_path}/#{@game_challenge.expect_picture_path}" @user_path = "#{Rails.root}/#{workspace_path}/#{@game_challenge.picture_path}" @original_path = "#{Rails.root}/#{workspace_path}/#{@game_challenge.original_picture_path}" @answer_picture = @game_challenge.expect_picture_path.nil? ? [] : get_dir_filename(@answer_path, type, @game.id) #@answer_picture = @answer_picture.sort {|x, y| conv.iconv(x) <=> conv.iconv(y)} if @answer_picture.present? @user_picture = @game_challenge.picture_path.nil? ? [] : get_dir_filename(@user_path, type, @game.id) #@user_picture = @user_picture.sort {|x, y| conv.iconv(x) <=> conv.iconv(y)} if @game_challenge.original_picture_path.blank? @orignal_picture = nil else @orignal_picture = @game_challenge.original_picture_path.nil? ? [] : get_dir_filename(@original_path, type, @game.id) #@orignal_picture = @orignal_picture.sort {|x, y| conv.iconv(x) <=> conv.iconv(y)} end end end # 同步更新最新代码 # 同步完成后,需要更新myshixun的commit_id为实训的commit最新值 # -------------------------- # 新思路,有冲突则则重置,没有冲突直接pull # identifier为password--- # -------------------------- # todo: TPI弹框的立即更新 def sync_codes shixun_tomcat = edu_setting('cloud_bridge') begin git_myshixun_url = repo_ip_url @myshixun.repo_path git_shixun_url = repo_ip_url @myshixun.shixun.try(:repo_path) git_myshixun_url = Base64.urlsafe_encode64(git_myshixun_url) git_shixun_url = Base64.urlsafe_encode64(git_shixun_url) # todo: identifier 是以前的密码,用来验证的,新版如果不需要,和中间层协调更改. params = {tpiID: "#{@myshixun.try(:id)}", tpiGitURL: "#{git_myshixun_url}", tpmGitURL: "#{git_shixun_url}", identifier: "xinhu1ji2qu3"} uri = "#{shixun_tomcat}/bridge/game/resetTpmRepository" res = uri_post uri, params if (res && res['code'] != 0) tip_exception("实训云平台繁忙(繁忙等级:95)") end shixun_new_commit = GitService.commits(repo_path: @myshixun.shixun.repo_path).first["id"] @myshixun.update_attributes!(commit_id: shixun_new_commit, reset_time: @myshixun.shixun.try(:reset_time)) if @game.challenge.st == 0 && @game.challenge.path.present? paths = @game.challenge.path.split(";") paths.each do |path| game_code_init @game.id, path.try(:strip) end end @path = @game.challenge.path # 更新完成后,弹框则隐藏不再提示 @myshixun.update_column(:system_tip, false) rescue Exception => e tip_exception("立即更新代码失败!#{e.message}") end end ## 给关卡打星星 def star shixun = Shixun.select([:id, :averge_star, :status]).where(id: params[:shixun_id]).first grades = Grade.where(user_id: current_user.id, container_id: @game.id, container_type: 'Star') if grades.exists? tip_exception("您已经评价过该实训") else @game.update_column(:star, params[:star].to_i) # 更新实训平均星星数值 averge_star = Game.find_by_sql("select ifnull(sum(g.star),0)/ifnull(count(*),1) as averge_star from (games g left join (myshixuns m join shixuns s on s.id = m.shixun_id) on m.id = g.myshixun_id) where star != 0 and s.id = #{shixun.id}").first.try(:averge_star) averge_star = averge_star.to_f || 5 shixun.update_column(:averge_star, averge_star.round(1)) # 随机生成10-100金币作为奖励 @gold = 0 # 加积分只针对已发布的实训 if shixun.status >= 2 @gold = rand(10..100) RewardGradeService.call(current_user, container_id: @game.id, container_type: 'Star', score: @gold) end end end ## 代码文件目录结构 def git_entries gpid = params[:gpid] @path = params[:path].try(:strip) rev = params[:rev] ? params[:rev] : "master" @trees = @g.trees(gpid, path: @path, rev: rev) unless @trees.count tip_exception("版本库异常") end end ## 是否可以查看答案,如果是管理员或者系统认证的老师则直接查看,不需要弹框 def answer challenge = Challenge.select([:answer, :id, :score, :st]).find(@game.challenge_id) # 这几种情况可以直接查看答案的:实训未发布;当前用户为实训管理员;已经查看过答案;平台认证的老师; @allowed = @shixun.status < 2 || @game.answer_open == 1 || current_user.shixun_identity(@shixun) <= User::EDU_CERTIFICATION_TEACHER uid_logger("-- is manager #{current_user.manager_of_shixun?(@shixun)}") @result = challenge.st == 0 ? challenge.try(:answer) : challenge.choose_answer end # 获取实践题答案 # GET: /tasks/:identifier/get_answer_info # 0 直接查看答案, 1 查看答案弹框, 2 答案详情弹框 def get_answer_info @challenge = @game.challenge @challenge_answers = @challenge.challenge_answers # 平台已认证的老师需要控制 @power = (@identity < User::EDU_GAME_MANAGER) # 只要跟这个实训相关的实训作业中有设置实训不公开答案,那么该实训下的该挑战就不能查看答案 homework_common_answer_public = @game.myshixun.shixun.homework_commons. where(course_id: User.current.as_student_courses.map(&:id)). map(&:answer_public).include?(false) if !@power if @challenge_answers.size == 0 tip_exception("无答案可以查看") elsif @challenge_answers.size == 1 if homework_common_answer_public tip_exception("该课程禁止查看答案") end # 未看答案,提示弹框 if @game.answer_open == 0 tip_exception(1, {answer_id: @challenge_answers.first.id, answer_score:@challenge_answers.first.score}) end else if @game.answer_open == 0 tip_exception(2, @challenge_answers.map{|a| {answer_id: a.id, answer_name: a.name, answer_score:a.score}}) end end end end # 获取选择题答案 def get_choose_answer @challenge = @game.challenge tip_exception("本接口只能获取选择题答案") if @challenge.st != 1 @power = (@identity < User::EDU_GAME_MANAGER) # 如果没权限,也没看过答案,则需要解锁 if @game.answer_open == 0 && !@power tip_exception(1, @challenge.choose_score) else @challenge_chooses = @challenge.challenge_chooses end end # 解锁实践题答案 # GET: /tasks/:identifier/get_answer_info?answer_id=? def unlock_answer @challenge = @game.challenge @answer = ChallengeAnswer.find(params[:answer_id]) challenge = @answer.challenge # 解锁需要本层级的答案是否需要扣分 points = challenge.challenge_answers.where(level: @game.answer_open + 1..@answer.level).sum(:score) deduct_score = ((points / 100.0) * challenge.score).to_i uid_logger("############金币数目: #{current_user.grade}") unless current_user.grade.to_i - deduct_score > 0 tip_exception("您没有足够的金币") end ActiveRecord::Base.transaction do begin # 积分消耗情况记录 score = challenge.st.zero? ? -deduct_score : -challenge.choose_score.to_i RewardGradeService.call(current_user, container_id: @game.id, container_type: 'Answer', score: score) # 通关查看答案 不扣 得分 answer_open = @challenge.st == 1 ? 1 : @answer.level if @game.status == 2 @game.update_attributes!(:answer_open =>answer_open) else # 扣除总分计算 answer_deduction = challenge.challenge_answers.where("level <= #{@answer.level}").sum(:score) @game.update_attributes!(:answer_open => answer_open, :answer_deduction => answer_deduction) end GameAnswer.create!(challenge_answer_id: @answer.id, user_id: current_user.id, game_id: @game.id, view_time: Time.now) rescue Exception => e uid_logger_error("#######金币扣除异常: #{e.message}") raise ActiveRecord::Rollback end end end # 解锁选择题答案 def unlock_choose_answer @challenge = @game.challenge score = @challenge.choose_score unless current_user.grade.to_i - score > 0 tip_exception("您没有足够的金币") end ActiveRecord::Base.transaction do begin # 积分消耗情况记录 RewardGradeService.call(current_user, container_id: @game.id, container_type: 'Answer', score: -score) # 通关查看答案 不扣 得分 if @game.status == 2 @game.update_attributes!(:answer_open => 1) else # 扣除总分计算 @game.update_attributes!(:answer_open => 1, :answer_deduction => 100) end @challenge_chooses = @challenge.challenge_chooses GameAnswer.create!(user_id: current_user.id, game_id: @game.id, view_time: Time.now) rescue Exception => e uid_logger_error("#######金币扣除异常: #{e.message}") raise ActiveRecord::Rollback end end end # 查看答案需要扣取金币 # 必须保证用户的金币数大于关卡的金币数 def answer_grade challenge = Challenge.select([:answer, :id, :score, :st]).find(@game.challenge_id) challenge_score = challenge.try(:score) final_score = @game.final_score @allowed_viewed = current_user.grade.to_i - challenge_score > 0 unless @allowed_viewed tip_exception("您没有足够的金币") end ActiveRecord::Base.transaction do begin if @game.answer_open == 0 # 如果这是第一次查看答案 if challenge.st == 0 @final_score = final_score - challenge_score # 积分消耗情况记录 RewardGradeService.call( current_user, container_id: @game.id, container_type: 'Answer', score: -challenge_score ) else @final_score = final_score - challenge.choose_score.to_i # 之所以不用final_score是因为过关后查看答案的final_score为0,但是记录需要记录扣除的分数 RewardGradeService.call( current_user, container_id: @game.id, container_type: 'Answer', score: -challenge.choose_score.to_i) end @game.update_attributes!(:answer_open => true, :final_score => final_score) end if challenge.st == 0 @answer = challenge.try(:answer) else @answer = challenge.choose_answer end @answer = # 更新当前用户的总金币数 @grade = User.where(:id => @game.user_id).pluck(:grade).first rescue Exception => e uid_logger_error("#######奖励金币异常: #{e.message}") raise ActiveRecord::Rollback end end end # 查看隐藏测试集 # REDO:有漏洞,通过game详情可以看到隐藏的测试集 def check_test_sets challenge = Challenge.select([:id, :score]).find(@game.challenge_id) user_grade = current_user.grade @minus_grade = challenge.score * 5 @allowed_viewed = user_grade >= @minus_grade if @allowed_viewed current_user.update_attribute(:grade, user_grade - @minus_grade) @game.update_attribute(:test_sets_view, true) # 扣分记录 Grade.create(:user_id => current_user.id, :container_id => @game.id, :score => -@minus_grade, :container_type => "testSet") max_query_index = @game.query_index.to_i testset_detail max_query_index, challenge else tip_exception(-1, "本操作需要扣除#{ @minus_grade }金币,您的金币不够了") end end # 恢复初始代码 # 注意path为当前打开文件的path def reset_original_code path = params[:path] # 恢复初始代码应该找tpm的版本库 repo_path = @myshixun.shixun.repo_path @content = git_fle_content(repo_path, path) tip_exception("初始代码为空,代码重置失败") if @content.nil? @language = judge_language_by_suffix(path) # 将tpm的代码内容同步更新到tpi update_file_content(@content, @myshixun.repo_path, path, current_user.git_mail, current_user.real_name, "reset_original_code") rescue Exception => e uid_logger_error("#{e.message}") tip_exception("初始化代码失败") end # 加载上次通过的代码 def reset_passed_code path = params[:path] game_code = GameCode.where(:game_id => @game.try(:id), :path => path).first if game_code.present? @content = game_code.try(:new_code) # @content = if @myshixun.mirror_name.select{|a| a.include?("MachineLearning") || a.include?("Python")}.present? && content.present? # content.gsub(/\t/, ' ') # else # content # end update_file_content(@content, @myshixun.repo_path, path, current_user.git_mail, current_user.real_name, "game passed reset") @language = judge_language_by_suffix(path) else tip_exception("代码重置失败,代码为空") end end # 获取版本库文件内容 # 注:如果本身path传错,内容肯定也为空;fork成功后,可能短时间内也获取不到版本库内容 # params[:status] 1: 目录树点击的请求 0:正常自动加载 # 返回参数status : -1 系统统一报错提示;-3 需要轮训重试,带retry参数;-4 立即重试 def rep_content challenge_path = @game.challenge.try(:path) if challenge_path.blank? tip_exception("代码获取异常,请检查实训模板的评测设置是否正确") end path = @game.challenge.try(:path).split(";")[0].strip() path = params[:path] || path status = params[:status].to_i path = path.try(:strip) @language = judge_language_by_suffix(path) uid_logger("--rep_content: path is #{path}") begin @content = git_fle_content(@myshixun.repo_path, path) || "" rescue Exception => e # 思路: 异常首先应该考虑去恢复 # retry为1表示已经轮训完成后还没有解决问题,这个时候需要检测异常 begin # 如果模板没有问题,则通过中间层检测实训仓库是否异常 # 监测版本库HEAD是否存在,不存在则取最新的HEAD gitUrl = repo_url @myshixun.repo_path gitUrl = Base64.urlsafe_encode64(gitUrl) shixun_tomcat = edu_setting('cloud_bridge') rep_params = {:tpiID => "#{@myshixun.id}", :tpiGitURL => "#{gitUrl}"} # 监测版本库HEAD是否存在,不存在则取最新的HEAD uri = "#{shixun_tomcat}/bridge/game/check" res = uri_post uri, rep_params uid_logger("repo_content to bridge: res is #{res}") # res值:0 表示正常;-1表示有错误;-2表示代码版本库没了 # if status == 0 && res # 版本库报错,修复不了 if res['code'] == -1 || res['code'] == -2 begin # GitService.delete_repository(repo_path: @myshixun.repo_path) if res['code'] == -1 project_fork(@myshixun, @shixun.repo_path, current_user.login) rescue Exception => e uid_logger_error("#{e.message}") tip_exception("#{e.message}") end end end rescue Exception => e uid_logger_error(e.message) if @myshixun.shixun.try(:status) < 2 tip_exception("代码获取异常,请检查实训模板的评测设置是否正确") else tip_exception(-3, "#{e.message}") end end # 如果报错了,并且retry 为1的时候,则fork一个新的仓库 if params[:retry].to_i == 1 project_fork(@myshixun, @shixun.repo_path, current_user.login) end tip_exception(0, e.message) end end # 编程题评测 def game_build sec_key = params[:sec_key] game_challenge = Challenge.select([:id, :position, :picture_path, :exec_time]).find(@game.challenge_id) # 更新评测次数 @game.update_column(:evaluate_count, (@game.evaluate_count.to_i + 1)) # 清空代码评测信息 msg = @game.run_code_message msg.update_attributes(:status => 0, :message => nil) if msg.present? # 更新时间是为了TPM端显示的更新,退出实训及访问实训的时候会更新,如果版本库地址不存在,重新去版本库中找 myshixuns_update = if @myshixun.repo_name.nil? g = Gitlab.client repo_name = g.project(@myshixun.gpid).path_with_namespace {repo_name: repo_name} else {updated_at: Time.now} end #logger.info("#############myshixuns_update: ##{myshixuns_update}") @myshixun.update_attributes!(myshixuns_update) gitUrl = repo_ip_url @myshixun.repo_path #logger.info("#############giturl: ##{gitUrl}") gitUrl = Base64.urlsafe_encode64(gitUrl) shixun_tomcat = edu_setting('cloud_bridge') step = game_challenge.try(:position) mirror_repository_limit = @shixun.mirror_repositories.where(main_type: 1).select(:resource_limit).try(:first).try(:resource_limit) # mirror表中很很大的脚本字段,所以单独查询一个字段效果更好 resource_limit = "echo 'ulimit -f #{mirror_repository_limit}' >> /root/.bashrc ; source /root/.bashrc\n" tpmScript = @shixun.evaluate_script.nil? ? "" : Base64.urlsafe_encode64((resource_limit + @shixun.evaluate_script).gsub("\r\n", "\n")) # status为2已经通过关,是重新评测 if @game.status == 2 resubmit = params[:resubmit] else # 重新评测不影响已通关的实训状态;first为第一次评测,通过前端JS轮询获取 @game.update_attributes!(status: 1) if params[:first].to_i == 1 end testSet = [] game_challenge.test_sets.each do |test_set| input = test_set.input.nil? ? "" : test_set.input.gsub("\r\n", "\n") output = test_set.output.nil? ? "" : test_set.output.gsub("\r\n", "\n") test_cases = {:input => input, :output => output, :matchRule => test_set.match_rule} testSet << test_cases end #logger.info("##############testSet: #{testSet}") testCases = Base64.urlsafe_encode64(testSet.to_json) unless testSet.blank? # 评测类型: 0,1,2 用于webssh的评测, 3用于vnc podType = @shixun.vnc_evaluate ? 3 : @shixun.webssh # 注意:这个地方的参数写的时候不能换行 content_modified = params[:content_modified] # 决定文件内容是否有修改,有修改如果中间层pull没有更新,则轮询等待更新 br_params = {:tpiID => "#{@myshixun.id}", :tpiGitURL => "#{gitUrl}", :buildID => "#{@game.id}", :instanceChallenge => "#{step}", :testCases => "#{testCases}", :resubmit => "#{resubmit}", :times => params[:first].to_i, :podType => podType, :content_modified => content_modified, :containers => "#{Base64.urlsafe_encode64(shixun_container_limit(@shixun))}", :persistenceName => @shixun.identifier, :tpmScript => "#{tpmScript}", :sec_key => sec_key, :timeLimit => game_challenge.exec_time, :isPublished => (@shixun.status < 2 ? 0 : 1), :trimBlank => @game.challenge&.ignore_space } # 评测有文件输出的需要特殊传字段 path:表示文件存储的位置 br_params['file'] = Base64.urlsafe_encode64({path: "#{game_challenge.picture_path}"}.to_json) if game_challenge.picture_path.present? # needPortMapping: web类型需要pod端口映射 br_params[:needPortMapping] = 8080 if @myshixun.mirror_name.include?("Web") # 私密仓库的设置 secret_rep = @shixun.shixun_secret_repository logger.info("############secret_rep: #{secret_rep}") if secret_rep&.repo_name secretGitUrl = repo_ip_url secret_rep.repo_path br_params.merge!({secretGitUrl: Base64.urlsafe_encode64(secretGitUrl), secretDir: secret_rep.secret_dir_path}) #logger.info("#######br_params:#{br_params}") end # 中间层交互 uri = "#{shixun_tomcat}/bridge/game/gameEvaluate" res = interface_post uri, br_params, 502, "gameEvaluate failed" @result = {status: 1, resubmit: resubmit, position: game_challenge.position, port: res['port'], had_done: @game.had_done} rescue Exception => e uid_logger("评测出错,详情:" + e.message) @result = {status: -1, message: "实训云平台繁忙(繁忙等级:502),请稍后刷新并重试", position: game_challenge.position, had_done: @game.had_done} end # 选择题评测 def choose_build Rails.logger.error("#################{params}") # 选择题如果通关了,则不让再评测 if @game.status == 2 raise Educoder::TipException.new("您已通过该关卡") end # 更新评测次数 @game.update_column(:evaluate_count, (@game.evaluate_count.to_i + 1)) game_challenge = Challenge.select([:id, :position]).find(@game.challenge_id) user_answer = params[:answer] challenge_chooses_count = user_answer.length choose_correct_num = 0 score = 0 had_passed = true test_sets = [] str = "" game_challenge.challenge_chooses.includes(:challenge_tags).each_with_index do |choose, index| # user_answer虽然是传的数组,但是可能存在多选择提的情况. user_answer_tran = user_answer[index].size > 1 ? user_answer[index].split("").sort.join("") : user_answer[index] standard_answer_tran = choose.standard_answer.size > 1 ? choose.standard_answer.split("").sort.join("") : choose.standard_answer correct = (user_answer_tran == standard_answer_tran) if str.present? str += "," end str += "('#{@game.id}', '#{choose.position}', '#{user_answer_tran}', '#{correct ? 1 : 0}', '#{@game.next_query_index}', '#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}', '#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}')" # 只要有一题错误就不能通关 had_passed = false if !correct choose_correct_num += 1 if correct # 全部通关的时候,需要对所得的总分记录 # 总分的记录应该是根据每一题累加,如果该题正确,则加分 score += choose.score if correct standard_answer = correct ? standard_answer_tran : -1 sin_test_set = {:result => correct, :actual_output => user_answer_tran, :standard_answer => standard_answer, :position => choose.position} test_sets << sin_test_set end # 批量插入评测结果 uid_logger("#------------chooice score: #{score}") sql = "INSERT INTO outputs (game_id, test_set_position, actual_output, result, query_index, created_at, updated_at) VALUES" + str ActiveRecord::Base.connection.execute sql # 没通关或者查看了答案通关的时候经验为0 # 通关但是查看了答案,评测的时候金币显示0(避免用户以为重复扣除),但是在关卡列表中金币显示负数 experience = 0 final_score = 0 # 如果本次答题全部正确,并且之前没有通关,则进行相应的奖励,每关只要有错题,则不进行任何奖励 # 注:扣除分数是在查看答案的时候扣除的 if had_passed && !@game.had_passed? @game.update_attributes(:status => 2, :end_time => Time.now) # TPM实训已发布并且没有查看答案 if @shixun.is_published? && @game.answer_open == 0 uid_logger("@@@@@@@@@@@@@@@@@chooice score: #{score}") # 查看答案的时候处理final_scor和扣分记录 experience = score reward_attrs = { container_id: @game.id, container_type: 'Game', score: score } RewardGradeService.call(@myshixun.owner, reward_attrs) @game.update_attribute(:final_score, score) final_score = score RewardExperienceService.call(@myshixun.owner, reward_attrs) end had_done = @game.had_done @myshixun.update_attribute(:status, 1) if had_done == 1 end grade = @myshixun.owner.try(:grade) # 更新实训关联的作品分数 TODO: 更新作业需要等作业模块开了再打开 # update_myshixun_work_score myshixun # 高性能取上一关、下一关 prev_game = @game.prev_of_current_game(@shixun.id, @game.myshixun_id, game_challenge.position) next_game = @game.next_game(@shixun.id, @game.myshixun_id, game_challenge.position) if had_passed next_game.update_column(:status, 0) if next_game.present? && next_game.status == 3 # 高性能取上一关、下一关 #prev_game = Game.prev_identifier(@shixun.id, @game.myshixun_id, game_challenge.position) #next_game = Game.next_game(@shixun.id, @game.myshixun_id, game_challenge.position) @result = {grade: grade, gold: final_score, experience: experience, challenge_chooses_count: challenge_chooses_count, choose_correct_num: choose_correct_num, test_sets: test_sets, prev_game: prev_game, next_game: next_game&.identifier} rescue Exception => e uid_logger("choose build failed #{e.message}") tip_exception(-1, e.message) end # 轮询获取状态 # resubmit是在file_update中生成的,从game_build中传入的 def game_status resubmit_identifier = @game.resubmit_identifier # 如果没有超时并且正在评测中 # 判断评测中的状态有两种:1、如果之前没有通关的,只需判断status为1即可;如果通过关,则判断game的resubmit_identifier是否更新 #uid_logger("################game_status: #{@game.status}") #uid_logger("################params[:resubmit]: #{params[:resubmit]}") #uid_logger("################resubmit_identifier: #{resubmit_identifier}") #uid_logger("################time_out: #{params[:time_out]}") sec_key = params[:sec_key] if (params[:time_out] == "false") && ((params[:resubmit].blank? && @game.status == 1) || (params[:resubmit].present? && (params[:resubmit] != resubmit_identifier))) # 代码评测的信息 running_code_status = @game.run_code_message.try(:status) running_code_message = @game.run_code_message.try(:message) render :json => { running_code_status: running_code_status, running_code_message: running_code_message } end uid_logger("##### resubmit_identifier is #{resubmit_identifier}") port = params[:port] score = 0 experience = 0 game_status = @game.status had_done = @game.had_done game_challenge = Challenge.select([:id, :score, :position, :shixun_id, :web_route, :show_type]).find(@game.challenge_id) if params[:resubmit].blank? # 非重新评测 if game_status == 2 # 通关 if @shixun.status > 1 score = @game.final_score # 查看答案的时候有对最终获得金币进行处理 experience = @game.final_score else score = 0 experience = 0 end end else # 重新评测 # 如果满足前面的条件,进入此处只可能是结果已返回并存入了数据库 if params[:resubmit] == resubmit_identifier # 本次重新评测结果已经返回并存入数据库 game_status = (@game.retry_status == 2 ? 2 : 0) # retry_status是判断重新评测的通关情况。2表示通关 end end # 实训的最大评测次数,这个值是为了优化查询,每次只取最新的最新一次评测的结果集 max_query_index = @game.query_index # max_query_index = @game.outputs.first.try(:query_index) # 区分评测过未评测过,未评测过按需求取数据 testset_detail max_query_index.to_i, game_challenge # 处理生成图片类型文件 picture = (game_challenge.show_type.to_i == -1 || @game.picture_path.nil?) ? 0 : @game.id # 针对web类型的实训 web_route = game_challenge.try(:web_route) server_url = @game.get_server_url if web_route.present? mirror_name = @shixun.mirror_name e_record = EvaluateRecord.where(:identifier => sec_key).first # 轮询结束,更新评测统计耗时 if game_status == 0 || game_status == 2 if e_record front_js = format("%.3f", (Time.now.to_f - e_record.try(:updated_at).to_f)).to_f consume_time = format("%.3f", (Time.now - e_record.created_at)).to_f e_record.update_attributes(:consume_time => consume_time, :front_js => front_js) end end #uid_logger("game is #{@game.id}, record id is #{e_record.try(:id)}, time is**** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}") # 记录前端总耗时 record_consume_time = e_record.try(:pod_execute) max_mem = e_record.try(:max_mem) # 实训制作者当前拥有的金币 grade = User.where(:id => @game.user_id).pluck(:grade).first # 高性能取上一关、下一关 # 上一关、下一关 prev_game = @game.prev_of_current_game(@shixun.id, @game.myshixun_id, game_challenge.position) next_game = @game.next_of_current_game(@shixun.id, @game.myshixun_id, game_challenge.position) if game_status == 2 @base_date = {grade: grade, gold: score, experience: experience, status: game_status, had_done: had_done, position: game_challenge.position, port: port, record_consume_time: record_consume_time, mirror_name: mirror_name, picture: picture, web_route: web_route, star: @game.star, next_game: next_game, prev_game: prev_game, max_mem: max_mem, server_url: server_url} end # 记录实训花费的时间 # REDO:需要添加详细的说明 def cost_time #return if @game.status >= 2 max_cost_time = (Time.now.to_i - @game.open_time.to_i) time = params[:time].to_i < @game.cost_time.to_i ? (@game.cost_time.to_i + params[:time].to_i) : params[:time].to_i cost_time = time > max_cost_time ? max_cost_time : time @game.update_attribute(:cost_time, cost_time) end # 同步challenge的更新时间 def sync_modify_time modify_time = Challenge.where(:id => @game.challenge_id).pluck(:modify_time).first @game.update_column(:modify_time, modify_time) sucess_status end # tpi弹框状态更新;true则不再显示;false每次刷新显示 def system_update myshixun = Myshixun.find(params[:myshixun_id]) myshixun.update_attribute(:system_tip, true) sucess_status end # 关闭webssh def close_webssh myshixun_id = @game.myshixun_id digest = @game.identifier + edu_setting('bridge_secret_key') digest_key = Digest::SHA1.hexdigest("#{digest}") begin shixun_tomcat = edu_setting('cloud_bridge') uri = "#{shixun_tomcat}/bridge/webssh/delete" Rails.logger.info("#{current_user} => cloese_webssh digest is #{digest}") params = {:tpiID => myshixun_id, :digestKey => digest_key, :identifier => @game.identifier} res = uri_post uri, params if res && res['code'].to_i != 0 raise("实训云平台繁忙(繁忙等级:110)") end rescue Exception => e Rails.logger.error(e) tip_exception("实训云平台繁忙, 关闭失败!") end end # tpi对于实训关卡的点赞或取消点赞 def plus_or_cancel_praise challenge = @game.challenge pt = PraiseTread.where(:praise_tread_object_id => challenge.id, :praise_tread_object_type => 'Challenge', :user_id => current_user, :praise_or_tread => 1).first # 如果当前用户已赞过,则不能重复赞 if pt.blank? PraiseTread.create!(:praise_tread_object_id => challenge.id, :praise_tread_object_type => 'Challenge', :user_id => current_user.id, :praise_or_tread => 1) if pt.blank? @praise = true else pt.destroy if pt.present? # 如果已赞过,则删掉这条赞(取消);如果没赞过,则为非法请求不处理 @praise = false end @praise_count = PraiseTread.where(:praise_tread_object_id => challenge.id, :praise_tread_object_type => 'Challenge', :praise_or_tread => 1).count end private # 评测测试机封装 def testset_detail max_query_index, challenge # 是否允许查看隐藏的测试集,以前的power @allowed_hidden_testset = @identity < User::EDU_GAME_MANAGER || @game.test_sets_view #解锁的用户 if max_query_index > 0 #uid_logger("max_query_index is #{max_query_index} game id is #{@game.id}, challenge_id is #{challenge.id}") @qurey_test_sets = TestSet.find_by_sql("SELECT o.code, o.actual_output, o.out_put, o.result, o.test_set_position, o.ts_time, o.ts_mem, o.query_index, t.is_public, t.input, t.output, o.compile_success FROM outputs o, games g, challenges c, test_sets t where g.id=#{@game.id} and c.id=#{challenge.id} and o.query_index=#{max_query_index} and g.id = o.game_id and c.id= g.challenge_id and t.challenge_id = c.id and t.position =o.test_set_position order by o.query_index ") @test_sets_count = @qurey_test_sets.count # 错误的测试集总数 @sets_error_count = 0 @qurey_test_sets.each do |set| @sets_error_count += 1 unless set.result end @last_compile_output = @qurey_test_sets.first['out_put'].gsub(/\n/, '
').gsub(/\t/, " \; \; \; \; \; \; \; \;") if @qurey_test_sets.first['out_put'].present? else # 没有评测过,第一次进来后的呈现方式 @qurey_test_sets = TestSet.find_by_sql("SELECT t.is_public, t.input, t.output, t.position FROM test_sets t where t.challenge_id = #{challenge.id}") end end # 实训选择题需要局部刷新或者显示的部分 def choose_container game_challenge, game, max_query_index # category 1: 单选题,其它的多选题(目前只有两种) challenge_chooses = game_challenge.challenge_chooses.includes(:challenge_questions) test_sets = [] @chooses = [] # 选择题测试集统计 challenge_chooses_count = challenge_chooses.count choose_correct_num = game.choose_correct_num(max_query_index) game_outputs = game.outputs.where(:query_index => max_query_index) # 判断用户是否有提交 had_submmit = game_outputs.present? # 判断选择题是否写了标准答案 has_answer = [] challenge_chooses.each do |choose| challenge_question = [] output = game_outputs.select{|game_output| game_output.test_set_position == choose.position}[0] unless game_outputs.blank? category = choose.category subject = choose.subject choose.challenge_questions.each do |question| position = question.position option_name = question.option_name challenge_question <<{:positon => position, :option_name => option_name} end # actual_output为空表示暂时没有评测答题,不允许查看 actual_output = output.try(:actual_output).try(:strip) #has_answer << choose.answer if choose.answer.present? # 标准答案处理,错误的不让用户查看,用-1替代 standard_answer = (actual_output.blank? || !output.try(:result)) ? -1 : choose.standard_answer result = output.try(:result) sin_test_set = {:result => result, :actual_output => actual_output, :standard_answer => standard_answer, :position => choose.position} sin_choose = {:category => category, :subject => subject, :challenge_question => challenge_question} @chooses << sin_choose test_sets << sin_test_set end @has_answer = true # 选择题永远都有答案 @choose_test_cases = {:had_submmit => had_submmit, :challenge_chooses_count => challenge_chooses_count, :choose_correct_num => choose_correct_num, :test_sets => test_sets} end def find_game @game = Game.find_by_identifier(params[:identifier]) if @game.blank? normal_status(404, "...") return end @myshixun = @game.myshixun end def find_shixun @shixun = Shixun.find(@myshixun.shixun_id) end # http://localhost:3000/tasks/hcie39pw2bjn # 可以访问条件:学员本身;管理员;TPM制作者 def allowed @identity = current_user.game_identity(@game) raise Educoder::TipException.new(403, "..") if @identity > User::EDU_GAME_MANAGER end # identity用户身份 def user_next_game(shixun, challenge, game, identity) next_game = game.next_of_current_game(shixun.id, game.myshixun_id, challenge.position) # 实训允许跳关 、 当前关卡已经通关、 用户是已认证的老师以上权限的人,允许跳关 if shixun.task_pass || game.status == 2 || identity <= User::EDU_CERTIFICATION_TEACHER next_game else nil end end # 更新关卡状态和一些学习进度 def update_game_parameter game game.update_attribute(:status, 0) if game.status == 1 # 第一次进入关卡更新时间 game.update_attributes(status: 0, open_time: Time.now) if game.open_time.blank? || game.status == 3 # 开启实训更新myshixuns的时间,方便跟踪用于的学习进度。 game.myshixun.update_column(:updated_at, Time.now) end # vnc连接 def get_vnc_link game begin shixun = game.myshixun.shixun shixun_tomcat = edu_setting('cloud_bridge') service_host = edu_setting('vnc_url') tpiGitURL = "#{edu_setting('git_address_domain')}/#{game.myshixun.repo_path}" uri = "#{shixun_tomcat}/bridge/vnc/getvnc" params = {tpiID: game.myshixun.id, :containers => "#{Base64.urlsafe_encode64(shixun_container_limit(shixun))}", tpiGitURL: tpiGitURL} res = uri_post uri, params if res && res['code'].to_i != 0 raise("实训云平台繁忙(繁忙等级:99)") end @vnc_url = res['showServer'] # @vnc_url = # if request.subdomain == "pre-newweb" || request.subdomain == "test-newweb" # # 无域名版本 # "http://#{service_host}:#{res['port']}/vnc_lite.html?password=headless" # else # # 有域名版本 # "https://#{res['port']}.#{service_host}/vnc_lite.html?password=headless" # end @vnc_evaluate = shixun.vnc_evaluate rescue Exception => e Rails.logger.error(e.message) end end # 删除pod def delete_vnc game myshixun_id = game.myshixun_id digest = game.identifier + edu_setting('bridge_secret_key') digest_key = Digest::SHA1.hexdigest("#{digest}") begin shixun_tomcat = edu_setting('cloud_bridge') uri = "#{shixun_tomcat}/bridge/vnc/delete" Rails.logger.info("#{current_user} => cloese_vnc digest is #{digest}") params = {:tpiID => myshixun_id, :digestKey => digest_key, :identifier => game.identifier} res = uri_post uri, params if res && res['code'].to_i != 0 raise("实训云平台繁忙(繁忙等级:110)") end end end # 根据文件后缀判断语言,默认使用shell渲染 def judge_language_by_suffix filename return "shell" if filename.blank? suffix = filename.split(".").last return "shell" if suffix.blank? case suffix.downcase when "py" then "python" when "h", "c" then "c" when "cpp", "java", "php", "html", "css", "scss", "go", "r", "graphql", "swift", "xml", "yaml", "json", "lua", "scheme", "less", "ini" then suffix.downcase when "coffee", "litcoffee" then "coffeescript" when "js" then "javascript" when "cs" then "csharp" when "kt" then "kotlin" when "md" then "markdown" when "sql" then "mysql" when "m", "mm" then "objective-c" when "pas" then "pascal" when "perl", "pl" then "perl" when "rb" then "ruby" when "rs", "rust" then "rust" when "sh" then "shell" else "shell" end end end