class GamesController < ApplicationController before_action :require_login, :check_auth before_action :find_game before_action :find_shixun, only: [:show, :answer, :rep_content, :choose_build, :game_build, :game_status] before_action :allowed #require 'iconv' include GamesHelper include ApplicationHelper def show uid_logger("--games show start") # 防止评测中途ajaxE被取消;3改成0是为了处理首次进入下一关的问题 @game.update_attribute(:status, 0) if @game.status == 1 @game.update_attributes(status: 0, open_time: Time.now) if @game.status == 3 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? 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) # 关卡点赞数, 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.try(:attributes), 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} if @shixun.vnc begin shixun_tomcat = edu_setting('cloud_bridge') service_host = edu_setting('vnc_url') uri = "#{shixun_tomcat}/bridge/vnc/getvnc" params = {tpiID: @myshixun.id, :containers => "#{Base64.urlsafe_encode64(shixun_container_limit(@shixun))}"} res = uri_post uri, params if res && res['code'].to_i != 0 raise("实训云平台繁忙(繁忙等级:99)") end # 无域名版本 #@vnc_url = "http://#{service_host}:#{res['port']}/vnc_lite.html?password=headless" # 有域名版本 @vnc_url = "https://#{res['port']}.#{service_host}/vnc_lite.html?password=headless" rescue Exception => e Rails.logger.error(e.message) end 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 # 查看效果 # 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 @type = "image" #conv = Iconv.new("GBK", "utf-8") @game_challenge = @game.challenge type = @game_challenge.show_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) if !@power if @challenge_answers.size == 0 tip_exception("无答案可以查看") elsif @challenge_answers.size == 1 # 未看答案,提示弹框 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 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 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 # # 文件更新;数据评测记录 # # 生成重新评测认证码 # # content_modified:0 表示文件没有更新;content_modified:1 表示文件有更新 # def file_update # path = params[:path].strip unless params[:path].blank? # myshixun = @game.myshixun # rev = params[:rev] ? params[:rev] : "master" # @content_modified = 0 # # params[:evaluate] 实训评测时更新必须给的参数,需要依据该参数做性能统计,其它类型的更新可以跳过 # # 自动保存的时候evaluate为0;点评测的时候为1 # if params[:evaluate] == 1 # record = EvaluateRecord.create!(:user_id => current_user.id, :shixun_id => myshixun.shixun_id, :game_id => @game.id) # uid_logger("-- game is #{@game.id}, record id is #{record.id}, time is **** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}") # student_work_time = format("%.3f", (Time.now.to_f - record.created_at.to_f)).to_f # record.update_attributes!(:student_work => student_work_time) # end # # 远程版本库文件内容 # last_content = GitService.file_content(repo_path: @repo_path, path: path)["content"] # last_content = tran_base64_decode64(last_content) # # content = if @myshixun.mirror_name.select{|a| a.include?("MachineLearning") || a.include?("Python")}.present? && params[:content].present? # params[:content].gsub(/\t/, ' ') # else # params[:content] # end # if content != last_content # @content_modified = 1 # code_file = @g.edit_file(myshixun.gpid, current_user.login, :content => content, :file_path => path, # :branch_name => rev, :commit_message => params[:evaluate] == 0 ? "auto commit" : "task commit") # uid_logger("-- file update #{code_file}") # # REDO:更新失败的处理 # raise("文件更新失败") unless code_file # end # # if record.present? # consume_time = format("%.3f", (Time.now.to_f - record.created_at.to_f)).to_f # record.update_attributes!(:file_update => consume_time) # end # # # status为2说明是重新评测 # if @game.status == 2 # code = CODES.sample(8).join # @resubmit = "#{code}_#{@myshixun.id}" # end # # if content != last_content && code_file.blank? # raise("实训平台繁忙(繁忙等级:81),请稍后刷新并重试") # end # rescue Exception => e # uid_logger("-- file update failed #{e.message}") # raise Educoder::TipException.new("#{e.message}") # 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? # 将tpm的代码内容同步更新到tpi update_file_content(@content, @myshixun.repo_path, path, current_user.mail, current_user.full_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.mail, current_user.full_name, "game passed reset") 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) uid_logger("--rep_content: path is #{path}") begin if @myshixun.repo_name.nil? g = Gitlab.client repo_name = g.project(@myshixun.gpid).path_with_namespace @myshixun.update_column(:repo_name, repo_name) @content = git_fle_content("#{repo_name}.git", path) || "" else @content = git_fle_content(@myshixun.repo_path, path) || "" end rescue Exception => e # 思路: 异常首先应该考虑去恢复 if params[:retry].to_i == 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 # 报错继续retry tip_exception(-3, "#{e.message}") end end end # 有异常,版本库获取不到代码,前端轮训30S后,调用retry == 1 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端显示的更新,退出实训及访问实训的时候会更新 @myshixun.update_column(:updated_at, Time.now) 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} testSet << test_cases end testCases = Base64.urlsafe_encode64(testSet.to_json) unless testSet.blank? # 注意:这个地方的参数写的时候不能换行 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 => @shixun.webssh, :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) } # 评测有文件输出的需要特殊传字段 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") # 中间层交互 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_of_current_game(@shixun.id, @game.myshixun_id, game_challenge.position) if had_passed # 高性能取上一关、下一关 #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} rescue Exception => e uid_logger("choose build failed #{e.message}") @result = [status: -1, contents: "#{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]).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.picture_path.nil? ? 0 : @game.id) # 针对web类型的实训 web_route = game_challenge.try(:web_route) 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} end # 记录实训花费的时间 # REDO:需要添加详细的说明 def cost_time cost_time = params[:time].to_i @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 end