# encoding=utf-8 class GamesService include ApplicationHelper include GamesHelper CODES = %W(1 2 3 4 5 6 7 8 9 A B C D E F G H J K L N M O P Q R S T U V W X Y Z) # 关卡详情 def show_game params, current_user game = Game.min.find_by_identifier(params[:identifier]) Rails.logger.info("game id is #{game.try(:id)}, current_user id is #{current_user.try(:id)}") if game.blank? || current_user.blank? return{:status => 404} end # 防止评测中途ajax被取消;3改成0是为了处理首次进入下一关的问题 game.update_attribute(:status, 0) if game.status == 1 game.update_attributes(:status => 0, :open_time => Time.now) if game.status == 3 # Redo:后续可以去掉,只是为了兼顾其它模块的耗时统计 myshixun = Myshixun.min.find(game.myshixun_id) shixun = Shixun.min.find(myshixun.shixun_id) unless ((myshixun.user_id == current_user.id || current_user.business? || current_user.id == shixun.try(:user_id) || current_user.is_certification_teacher) && (shixun.operable?)) || current_user.admin? Rails.logger.info("######403???") return{:status => 403} end game_challenge = Challenge.min.find(game.challenge_id) has_answer = game_challenge.answer.blank? ? false : true game_challenge.answer = nil if game_challenge.st == 1 game_challenge.score = game_challenge.choose_score.to_i end # st:判断是选择类型还是实训类型 st = game_challenge.st game_count = myshixun.games.count discusses_count = (current_user.admin? ? shixun.discusses.count : shixun.discusses.where("hidden = false or user_id = :user_id", user_id: current_user.id).count) mirror_name = myshixun.mirror_name user = myshixun.owner username = user.show_name image_url = url_to_avatar(user) user_url = "/users/#{user.login}" is_teacher = (user.user_extensions.identity == 0) tpm_identifier = shixun.try(:identifier) # 实训超时设置 time_limit = shixun.exec_time # 高性能取上一关、下一关 prev_game = Game.prev_identifier(shixun.id, game.myshixun_id, game_challenge.position) next_game = if shixun.vnc || current_user.is_certification_teacher || shixun_manager(shixun, current_user) || game.status || shixun.task_pass Game.next_game(shixun.id, game.myshixun_id, game_challenge.position).try(:identifier) end # 该参数是为了判断是否需要加载loading @is_subject = params[:is_subject] # 关卡点赞数, praise_or_tread = 1则表示赞过 praise_count = PraiseTread.where(:praise_tread_object_id => game_challenge.id, :praise_tread_object_type => "Challenge", :praise_or_tread => 1).count user_praise = PraiseTread.where(:praise_tread_object_id => game_challenge.id, :praise_tread_object_type => "Challenge", :user_id => current_user.id, :praise_or_tread => 1).present? ? true : false # 实训的最大评测次数,这个值是为了优化查询,每次只取最新的最新一次评测的结果集 max_query_index = game.query_index - 1 # 判断实训是否全部通关 had_done = game.had_done # 统计时间 record = EvaluateRecord.where(:game_id => game.id).first.try(:consume_time) # 实训制作者当前拥有的金币 grade = myshixun.owner.try(:grade) # power判断用户是否有权限查看隐藏测试集(TPM管理员;平台认证的老师;花费金币查看者) -1 表示不能解锁 0 表示需要付费解锁 1表示可以看 # myshixun_manager myshixun_manager = shixun_manager(shixun, current_user) || (current_user.is_certification_teacher) power = (myshixun_manager || game.test_sets_view ) ? 1 : (shixun.test_set_permission ? 0 : -1) # 选择题和编程题公共部分 container = {:st => st, :discusses_count => discusses_count, :game_count => game_count, :myshixun => myshixun.try(:attributes), :challenge => game_challenge.try(:attributes), :game => game.try(:attributes), :shixun => shixun.try(:attributes), :record => record, :grade => grade, :prev_game => prev_game, :next_game => next_game, :username => username, :image_url => image_url, :user_url => user_url, :praise_count => praise_count, :user_praise => user_praise, :time_limit => time_limit, :tomcat_url => Redmine::Configuration['tomcat_php'], :is_teacher => is_teacher, :power => power, :myshixun_manager => myshixun_manager} if shixun.vnc begin shixun_tomcat = Redmine::Configuration['shixun_tomcat'] service_host = Redmine::Configuration['tomcat_php'] uri = "#{shixun_tomcat}/bridge/vnc/getvnc" params = {tpiID: myshixun.id, :containers => "#{Base64.urlsafe_encode64(container_limit(shixun.mirror_repositories))}"} res = uri_exec uri, params if res && res['code'].to_i != 0 raise("实训云平台繁忙(繁忙等级:99)") end url = "#{service_host}" +":#{res['port']}/vnc_lite.html?password=headless" Rails.logger.info("66666666sssssss#{url}") container = container.merge(:vnc_url => url) Rails.logger.info("777777666sssssss#{container}") rescue Exception => e Rails.logger.error(e.message) end end if st == 0 && shixun.status == 2 && myshixun_manager zip_path = Gitlab.endpoint.to_s + "/projects/" + myshixun.gpid.to_s + "/repository/archive?&private_token=" + Gitlab.private_token.to_s container = container.merge(:zip_path => zip_path) end # 区分选择题和编程题,st:0编程题; if st == 0 gitUrl = gitlab_url myshixun gitUrl = Base64.urlsafe_encode64(gitUrl) shixun_tomcat = Redmine::Configuration['shixun_tomcat'] params = {:tpiID => "#{myshixun.id}", :tpiGitURL => "#{gitUrl}"} # 判断tpm是否修改了 tpm_modified = repository_is_modified(myshixun, shixun.try(:gpid)) # 判断TPM和TPI的版本库是否被改了 tpm_cases_modified = (game_challenge.modify_time != game.modify_time ? true : false) # modify_time 决定TPM测试集是否有更新 # tpm_script_modified = (shixun.reset_time > myshixun.reset_time ? true : false) # 新结构脚本是实时传送的,所以不会影响 # 区分评测过未评测过,未评测过按需求取数据 sql = if max_query_index > 0 "SELECT b.code, b.actual_output, b.out_put, b.result, b.compile_success, a.is_public, a.input, a.output FROM (SELECT position, input, output, challenge_id, is_public FROM test_sets where challenge_id=#{game_challenge.id}) a LEFT JOIN (SELECT result, test_set_position, g.challenge_id, o.actual_output, o.out_put, o.compile_success, o.code FROM outputs o left join games g on g.id=o.game_id WHERE game_id=#{game.id} and query_index = #{max_query_index} ) b ON b.challenge_id = a.challenge_id and b.test_set_position = a.position" else # 这个地方如果加字段的话一定注意test_set_static_data方法,因为这个地方的字段数在下面有判断 "SELECT t.is_public, t.input, t.output, t.position FROM games g ,challenges c,test_sets t WHERE g.id=#{game.id} and c.id= g.challenge_id and t.challenge_id = c.id" end qurey_test_sets = TestSet.find_by_sql(sql) # 测试集统计及处理 unless qurey_test_sets.blank? check_power = (power == 1 || game.test_sets_view) test_sets = test_set_static_data(qurey_test_sets, check_power) end test_sets_count = qurey_test_sets.size had_test = Output.where(:game_id => game.id, :query_index => max_query_index) had_test_count = had_test.count had_passed_testsests_error_count = had_test.blank? ? 0 : had_test.select{|had_test| had_test.result == false}.count had_passed_testsests_error_count = had_test_count - had_passed_testsests_error_count error_position = had_test.blank? ? nil : had_test.select{|had_test| had_test.result == false}.last Rails.logger.info("latest output id is #{error_position.id unless error_position.blank?}") latest_output = error_position.try(:out_put).gsub(/\n/, '
').gsub(/\t/, " \; \; \; \; \; \; \; \;") unless error_position.try(:out_put).blank? Rails.logger.warn(latest_output) output_hash = {:test_sets => test_sets}.merge(:had_test_count => had_test_count, :test_sets_count => test_sets_count, :had_passed_testsests_error_count => had_passed_testsests_error_count) multi_webssh = shixun.webssh == 2 && shixun.multi_webssh result = {:tpm_modified => tpm_modified, :tpm_cases_modified => tpm_cases_modified, :hide_code => shixun.hide_code, :forbid_copy => shixun.forbid_copy, :output_sets => output_hash, :latest_output => latest_output, :mirror_name => mirror_name, :multi_webssh => multi_webssh, :has_answer => has_answer}.merge(container) else # 选择题类型的 # 该方法多个地方调用,比如show、评测 # 最后一个字段true表示只显示数据,false表示可能有添加数据 result_container = choose_container(game_challenge, game, max_query_index, true) return result_container.merge(container) end end # output为testset的实际输出 # out_put为顶端结果显示输出 power 查看测试集的权限 def test_set_static_data test_sets, power test_result = [] unless test_sets.blank? test_sets.each do |test_set| # 第一次刷新或者没有评测的的时候test_set只需取四个字段,所以取了count actual_output = test_set.attributes.count > 4 ? test_set.try(:actual_output) : nil result = test_set.attributes.count > 4 ? test_set.try(:result) : nil compile_success = test_set.attributes.count > 4 ? test_set.try(:compile_success) : nil status = test_set.attributes.count > 4 ? test_set.try(:code) : nil #actual_output = Base64.encode64(actual_output) actual_output = (compile_success.to_s == "0" && status.to_s == "-1" ? "编译失败,请在测试结果中查看具体的错误信息" : actual_output) public_result = {:is_public => (test_set.is_public ? 1 : 0), :result => result, :actual_output => actual_output, :compile_success => compile_success} # 测试集公开的话才返回input结果 (test_set.is_public || power) && public_result.merge!({:input => test_set.input, :output => test_set.output}) Rails.logger.info("#######################test_set: #{public_result}") test_result << public_result.to_json end end test_result = test_result.blank? ? test_result : test_result.join(",") return test_result.gsub(/<\/script>/, '') end # 获取TPI关卡内容 def challenges params result = [] myshixun = Myshixun.find(Game.where(:identifier => params[:identifier]).pluck(:myshixun_id).first) shixun = Shixun.select([:id, :status]).find(myshixun.shixun_id) games = myshixun.games.includes(:challenge).reorder("challenges.position") games.each do |game| game_status = game.try(:status) challenge = game.challenge if challenge.st == 0 # 实战类型 if game_status == 2 # 通关了则取实际得分,没通关则取总分 gold = (shixun.status <= 1) ? 0 : game.final_score.to_i # 只要过关了,查看了答案经验值就是0,金币是负数 experience = (shixun.status <= 1 || game.final_score.to_i < 0) ? 0 : challenge.score.to_i challenge_tags_count = (shixun.status <= 1) ? 0 : challenge.challenge_tags.count else gold = challenge.score.to_i experience = gold challenge_tags_count = challenge.challenge_tags.count end else if game_status == 2 gold = (shixun.status <= 1) ? 0 : game.final_score.to_i experience = (shixun.status <= 1 || game.final_score.to_i < 0) ? 0 : challenge.choose_score.to_i challenge_tags_count = (shixun.status <= 1 || game.final_score.to_i <= 0) ? 0 : challenge.choose_tags_num else # 选择题只有在全对的时候才会获取final score总分,错任何一个题final_score就为0 gold = challenge.choose_score.to_i experience = challenge.choose_score.to_i challenge_tags_count = challenge.choose_tags_num end end result << {:position => challenge.position, :subject => challenge.subject, :tag_count => challenge_tags_count, :gold => gold, :experience => experience, :status => game.status, :star => game.star, :identifier => game.identifier} end result end # 评论打星星 def star params, current_user game = Game.find_by_identifier(params[:identifier]) shixun = Shixun.select([:id, :status]).find(params[:shixun_id]) # 如果已经评星,则不能重复评 grade = Grade.where(:user_id => current_user.id, :container_id => game.id, :container_type => 'Star').first if grade.present? return{:reward_code => -1} else game.update_column(:star, params[:star].to_i) # 随机生成10-100金币 code = rand(10..100) # 积分消耗情况记录;加积分之针对已发布的实训 if shixun.status >= 2 reward_grade(current_user, game.id, 'Star', code) return{:reward_code => code} else return{:reward_code => 0} end end end # TPI版本库文件目录结构 def entries params g = Gitlab.client gpid = params[:gpid] path = params[:path].try(:strip) rev = params[:rev] ? params[:rev] : "master" result = [] trees = g.trees(gpid, path: path, rev: rev) trees.each do |tree| # 去掉第一行的"/" new_path = File.join(path,tree.name).reverse.chomp("/").reverse result << {:name => tree.name, :path => new_path, :kind => tree.type == 'tree' ? 'dir' : 'file'} end return result end def answer params, current_user game_base params[:identifier] challenge = Challenge.select([:answer, :id, :score, :st]).find(@game.challenge_id) is_teacher = (current_user.user_extensions.identity == 0) is_certification_teacher = current_user.is_certification_teacher # 这几种情况可以直接查看答案的:实训未发布;当前用户为实训管理员;已经查看过答案;平台认证的老师; if (@shixun.status < 2 || shixun_manager(@shixun, current_user) || @game.answer_open == 1 || is_certification_teacher) Rails.logger.info("answer#{params[:identifier]}#### user is #{current_user.id}, status is #{@shixun.status}, manager is #{shixun_manager(@shixun, current_user)}, open #{@game.answer_open}, cer #{is_certification_teacher}") if challenge.st == 0 result = challenge.try(:answer) else result = [] challenge.challenge_chooses.each do |choose| result << {:position => choose.position, :answer => (choose.answer.blank? ? choose.standard_answer : choose.answer)} end end return {:view_answer => true, :answer => result} else return {:is_teacher => is_teacher, :view_answer => false} # view_answer = false end end # 查看答案需要扣取金币 # 必须保证用户的金币数大于关卡的金币数 def answer_grade params, current_user game_base params[:identifier] challenge = Challenge.select([:answer, :id, :score, :st]).find(@game.challenge_id) challenge_score = challenge.try(:score) final_score = @game.final_score if current_user.grade.to_i - challenge_score > 0 if @game.answer_open == 0 # 如果这是第一次查看答案 if challenge.st == 0 final_score = final_score - challenge_score # 积分消耗情况记录 reward_grade(current_user, @game.id, 'Answer', -challenge_score) else final_score = final_score - challenge.choose_score.to_i # 之所以不用final_score是因为过关后查看答案的final_score为0,但是记录需要记录扣除的分数 reward_grade(current_user, @game.id, 'Answer', -challenge.choose_score.to_i) end @game.update_attributes!(:answer_open => true, :final_score => final_score) end else # 金币不足 raise("0") end if challenge.st == 0 answer = challenge.try(:answer) else answer = [] challenge.challenge_chooses.each do |choose| answer << {:position => choose.position, :answer => (choose.answer.blank? ? choose.standard_answer : choose.answer)} end end return {:answer => answer, :final_score => final_score, :grade => @myshixun.owner.try(:grade)} end # 查看隐藏测试集 def check_test_sets params, current_user game = Game.select([:id, :challenge_id, :test_sets_view]).find_by_identifier(params[:identifier]) if game.test_sets_view == true return {:status => -1, :message => "已解锁,请误重复操作!"} end challenge = Challenge.select([:id, :score]).find(game.challenge_id) user_grade = current_user.grade minus_grade = challenge.score * 5 max_query_index = game.query_index - 1 # 区分评测过未评测过,未评测过按需求取数据 if max_query_index > 0 qurey_test_sets = TestSet.find_by_sql("SELECT o.code, o.actual_output, o.out_put, o.result, o.compile_success, o.test_set_position, o.query_index,t.is_public,t.input, t.output, g.id as game_id, c.id as challenge_id FROM outputs o,games g ,challenges c,test_sets t where g.id=#{game.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 ") else qurey_test_sets = TestSet.find_by_sql("SELECT t.is_public,t.input, t.output,t.position FROM games g ,challenges c,test_sets t where g.id=#{game.id} and c.id= g.challenge_id and t.challenge_id = c.id ") end test_sets = test_set_static_data(qurey_test_sets, true) if user_grade >= minus_grade current_user.update_attribute(:grade, user_grade - minus_grade) game.update_attribute(:test_sets_view, true) # 扣分记录 gardes = Grade.create(:user_id => current_user.id, :container_id => game.id, :score => -minus_grade, :container_type => "testSet") gardes.attributes.merge!({test_sets: test_sets}) else return {:status => -1, :message => "本操作需要扣除#{ minus_grade }金币,您的金币不够了"} end end # 文件更新;数据评测记录 # 生成重新评测认证码 # content_modified:0 表示文件没有更新;content_modified:1 表示文件有更新 def file_update params, current_user game_base params[:identifier] path = params[:path].strip unless params[:path].blank? rev = params[:rev] ? params[:rev] : "master" content_modified = 0 ActiveRecord::Base.transaction do # params[:evaluate] 实训评测时更新必须给的参数,需要依据该参数做性能统计,其它类型的更新可以跳过 # 自动保存的时候evaluate为0;点评测的时候为1 if params[:evaluate] == 1 sec_key = generates_identifier(EvaluateRecord, 10) record = EvaluateRecord.create!(:user_id => current_user.id, :shixun_id => @myshixun.shixun.id, :game_id => @game.id, :identifier => sec_key) Rails.logger.warn("##game is is #{@game.id}, record id is #{record.id}, time is**** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}") # @myshixun.student_works.update_all(:update_time => Time.now) if !@myshixun.student_works.blank? 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 @g = Gitlab.client last_content = @g.files(@myshixun.gpid, path, "master").try(: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 => "master", :commit_message => (params[:evaluate] == 1 ? "commit by author" : "auto commit" )) end # REDO:是否有读写分离的问题 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("update file failed") else return {:success => "success", :resubmit => resubmit ,:content_modified => content_modified, sec_key: record.try(:identifier)} end end rescue Exception => e Rails.logger.error("file update failed #{e.message}") return {:status => -1, :success => "fail", :message => "实训云平台繁忙(繁忙等级:81),请稍后刷新并重试", :position => @game.challenge.position, :had_done => @game.had_done} end # 恢复初始代码 # 注意path为当前打开文件的path def reset_original_code params, current_user g = Gitlab.client path = params[:path] begin file_base params[:identifier] shixun = Shixun.select(:gpid).find(@myshixun.shixun_id) content = g.files(shixun.gpid, path, "master").try(:content) content = tran_base64_decode64(content) g.edit_file(@myshixun.gpid, current_user.login, :content => content, :file_path => path, :branch_name => "master", :commit_message => "reset_original_code") if content.nil? raise("初始代码为空,代码重置失败") end return content rescue Exception => e raise("#{e.message}") end end def reset_passed_code params, current_user g = Gitlab.client path = params[:path] game= Game.select([:id, :myshixun_id]).where(:identifier => params[:identifier]).first myshixun = Myshixun.find(game.myshixun_id) 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 g.edit_file(myshixun.try(:gpid), current_user.login, :content => content, :file_path => path, :branch_name => "master", :commit_message => "game passed reset") return content else return {:status => -1, :message => "重置失败,代码为空"} end end # 获取版本库文件内容 # 通过版本库的默认分支可以判断版本库是否正常 # 注:如果本身path传错,内容肯定也为空;fork成功后,可能短时间内也获取不到版本库内容 # status =0 表示获取默认打开的内容,1表示右侧目录结构中获取的内容 def rep_content params, current_user file_base params[:identifier] # current_user: fork不应该是用当前用户去fork current_user = @myshixun.owner g = Gitlab.client gpid = @myshixun.try(:gpid) path = @game.challenge.try(:path).split(";")[0].strip() path = params[:path] ? params[:path] : path shixun_gpid = params[:shixun_gpid] status = params[:status] path = path.try(:strip) rev = params[:rev] ? params[:rev] : "master" Rails.logger.warn("path is #{path}") content = g.files(gpid, path, rev).try(:content) if content.blank? # 监测版本库HEAD是否存在,不存在则取最新的HEAD gitUrl = gitlab_url @myshixun gitUrl = Base64.urlsafe_encode64(gitUrl) shixun_tomcat = Redmine::Configuration['shixun_tomcat'] rep_params = {:tpiID => "#{@myshixun.id}", :tpiGitURL => "#{gitUrl}"} # 监测版本库HEAD是否存在,不存在则取最新的HEAD uri = "#{shixun_tomcat}/bridge/game/check" res = uri_exec uri, rep_params # res值:0 表示正常;-1表示有错误;-2表示代码版本库没了 if (status == 0 && res && res['code'] == -2 && params[:retry].to_i == 1) # 没有版本库则删除现有的版本库关联,重新fork一个版本库 g = Gitlab.client if current_user.gid.nil? s = Trustie::Gitlab::Sync.new s.sync_user(current_user) end gpid = sync_gitlab_rep @myshixun, shixun_gpid, current_user.try(:gid) content = g.files(gpid, path, rev).content else if (res && res['code'] != 0) Rails.logger.error("get rep_content failed: bridge return !0") # raise(status) return nil end end # gitlab缺陷:forked完成,短暂时间内取不了内容的,所以做一个小轮询,间隔0.2秒 # 超过3秒则失败,需通过页面刷新 # for i in 0..15 do # sleep(0.2) # content = g.files(gpid, path, rev).content # break if content.present? # end end content = tran_base64_decode64(content) if content.present? return content end # 编程题评测 def game_build params # 三类实训只取基础部分数据 game = Game.select([:myshixun_id, :status, :challenge_id, :id, :evaluate_count]).find_by_identifier(params[:identifier]) myshixun = Myshixun.select([:updated_at, :gpid, :id, :shixun_id]).find(game.myshixun_id) shixun = Shixun.select([:id, :evaluate_script, :webssh, :exec_time, :sigle_training, :identifier, :status]).find(myshixun.shixun_id) game_challenge = Challenge.select([:id, :position, :picture_path]).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 = gitlab_url myshixun gitUrl = Base64.urlsafe_encode64(gitUrl) taskId = game.id shixun_tomcat = Redmine::Configuration['shixun_tomcat'] 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没有更新,则轮询等待更新 params = {:tpiID => "#{myshixun.id}", :tpiGitURL => "#{gitUrl}", :buildID => "#{taskId}",:instanceChallenge => "#{step}", :testCases => "#{testCases}", :resubmit => "#{resubmit}", :times => params[:first].to_i, :podType => shixun.webssh, :containers => "#{Base64.urlsafe_encode64(container_limit(shixun.mirror_repositories))}", :tpmScript => "#{tpmScript}", :timeLimit => "#{shixun.exec_time}", :content_modified => content_modified, :persistenceName => shixun.identifier, :isPublished => (shixun.status < 2 ? 0 : 1), :sec_key => params[:sec_key]} # 评测有文件输出的需要特殊传字段 path:表示文件存储的位置 params['file'] = Base64.urlsafe_encode64({:path => "#{game_challenge.picture_path}"}.to_json) if game_challenge.picture_path.present? # needPortMapping: web类型需要pod端口映射 params[:needPortMapping] = 8080 if myshixun.mirror_name.include?("Web") # 中间层交互 if shixun.sigle_training uri = "#{shixun_tomcat}/bridge/game/persistence/gameEvaluate" else uri = "#{shixun_tomcat}/bridge/game/gameEvaluate" end res = interface_post uri, params, 502 # 单评测类型(比较快的类型,实时返回结果,即不用中间层再回调trainint_task_status) if res['syncResult'].present? sigle_trainint_data res, myshixun, game end # ----单测模式end return {:result => "success", :resubmit => resubmit, :ableToCreate => res['ableToCreate'], :waitNum => res['waitNum'], :waitingTime => res['waitingTime'], :position => game_challenge.position, :port => res['port'], :had_done => game.had_done} rescue Exception => e Rails.logger.error("评测出错,详情:" + e.message) return {:result => 'fail', :contents =>"实训云平台繁忙(繁忙等级:502),请稍后刷新并重试", :position => game_challenge.position, :had_done => game.had_done} end # 单评测类型(比较快的类型,实时返回结果,即不用中间层再回调trainint_task_status) def sigle_trainint_data res, myshixun, game begin t1 = Time.now jsonTestDetails = JSON.parse(res['syncResult']['jsonTestDetails']) Rails.logger.info("res is #{res} ### jsonTestDetails is #{jsonTestDetails}") timeCost = JSON.parse(res['syncResult']['timeCost']) brige_end_time = Time.parse(timeCost['evaluateEnd']) if timeCost['evaluateEnd'].present? return_back_time = format("%.3f", ( t1.to_f - brige_end_time.to_f)).to_f status = jsonTestDetails['status'] game_id = jsonTestDetails['buildID'] Rails.logger.info("sigle training_task_status start#1**#{game_id}**** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}") resubmit = jsonTestDetails['resubmit'] outPut = tran_base64_decode64(jsonTestDetails['outPut']) jenkins_testsets = jsonTestDetails['msg'] compile_success = jsonTestDetails['compileSuccess'] Rails.logger.info(outPut) challenge = game.challenge if challenge.picture_path.present? pics = res[:tpiRepoPath] game.update_column(:picture_path, pics) end Rails.logger.info("training_task_status start#2**#{game_id}**** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}") unless jenkins_testsets.blank? jenkins_testsets.each do |j_test_set| Rails.logger.info("j_test_set: ############## #{j_test_set}") actual_output = tran_base64_decode64(j_test_set['output']) Rails.logger.info "actual_output:################################################# #{actual_output}" Output.create!(:code => status, :game_id => game_id, :out_put => outPut, :test_set_position => j_test_set['caseId'], :actual_output => actual_output, :result => j_test_set['passed'].to_i, :query_index => game.query_index, :compile_success => compile_success.to_i) end end Rails.logger.info("#############status: #{status}") record = EvaluateRecord.where(:game_id => game_id).first Rails.logger.info("training_task_status start#3**#{game_id}**** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}") # status:0表示评测成功 if status == "0" if !resubmit.blank? game.update_attributes!(:retry_status => 2, :resubmit_identifier => resubmit) #if game.had_done == 1 challenge.path.split(";").each do |path| game_passed_code(game.id, path.try(:strip), myshixun.try(:gpid), 1) end #end else game.update_attributes!(:status => 2, :end_time => Time.now, :accuracy => format("%.4f", 1.0 / game.query_index)) myshixun.update_attributes!(:status => 1) if game.had_done == 1 challenge.path.split(";").each do |path| game_passed_code(game.id, path, myshixun.try(:gpid), 1) end if !game.answer_open && (challenge.shixun.try(:status) > 1) # 如果没有查看答案,则获得该关卡得分 reward_grade(game.user, game.id, 'Game', challenge.score) reward_experience(game.user, game.id, 'Game', challenge.score) game.update_attributes!(:final_score => challenge.score) end # 更新实训关联的作品分数 update_myshixun_work_score myshixun end # 如果过关了,下一关的状态是3(为开启),则需要把状态改成1(已开启) # next_game = game.next_game next_game = Game.next_game(myshixun.shixun_id, game.myshixun_id, challenge.position) next_game.update_column(:status, 0) if next_game.present? && next_game.status == 3 else # status == "-1" 表示返回结果错误 if !resubmit.blank? game.update_attributes!(:retry_status => 1, :resubmit_identifier => resubmit) else game.update_attributes!(:status => 0) end end test_cases_time = format("%.3f", (Time.now.to_f - t1.to_f)).to_f if record.present? consume_time = format("%.3f", (Time.now - record.created_at)).to_f record.update_attributes!(:consume_time => consume_time, :git_pull => timeCost['pull'] , :create_pod => timeCost['createPod'], :pod_execute => timeCost['execute'], :test_cases => test_cases_time, :brige => timeCost['evaluateAllTime'], :return_back => return_back_time) end end end # 选择题评测 def choose_build params # 三类实训只取基础部分数据 game = Game.select([:myshixun_id, :status, :challenge_id, :id, :answer_open, :final_score, :identifier, :evaluate_count]).find_by_identifier(params[:identifier]) # 更新评测次数 game.update_column(:evaluate_count, (game.evaluate_count.to_i + 1)) game.challenge.shixun.increment!(:evaluate_count) # 选择题如果通关了,则不让再评测 if game.status == 2 return {:message => "您已通过该关卡"} end myshixun = Myshixun.select([:updated_at, :gpid, :id, :shixun_id, :status, :user_id]).find(game.myshixun_id) shixun = Shixun.select([:id, :evaluate_script, :webssh, :status]).find(myshixun.shixun_id) 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 total_score = 0 had_passed = true test_sets = [] str = "" game_challenge.challenge_chooses.includes(:challenge_tags).each_with_index do |choose, index| 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) ? true : false if str.present? str += "," end str += "('#{game.id}', '#{choose.position}', '#{user_answer_tran}', '#{correct ? 1 : 0}', '#{game.query_index}', '#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}', '#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}')" # Output.create(:game_id => @game.id, :test_set_position => choose.position, :actual_output => params[:answer][index], :result => correct, :query_index => @game.query_index) # 只要有一题错误就不能通关 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 # 批量插入评测结果 Rails.logger.warn("sql str is #{str}") 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 # 查看答案的时候处理final_scor和扣分记录 experience = score reward_grade(myshixun.owner, game.id, 'Game', score) game.update_attribute(:final_score, score) final_score = score reward_experience(myshixun.owner, game.id, 'Game', score) end end had_done = game.had_done myshixun.update_attribute(:status, 1) if had_done == 1 grade = myshixun.owner.try(:grade) # 更新实训关联的作品分数 update_myshixun_work_score myshixun # 高性能取上一关、下一关 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) # 如果过关了,下一关的状态是3(为开启),则需要把状态改成1(已开启) next_game.update_column(:status, 0) if next_game.present? && next_game.status == 3 return {:grade => grade, :gold => final_score, :experience => experience, :had_submmit => true, :challenge_chooses_count => challenge_chooses_count, :choose_correct_num => choose_correct_num, :test_sets => test_sets, :prev_game => prev_game, :next_game => next_game.try(:identifier)} end # 实训选择题需要局部刷新或者显示的部分 def choose_container game_challenge, game, max_query_index, just_show # category 1: 单选题,其它的多选题(目前只有两种) challenge_chooses = game_challenge.challenge_chooses.includes(:challenge_questions) test_sets = [] choose_test_cases = [] chooses = [] # 选择题测试集统计 challenge_chooses_count = challenge_chooses.count choose_correct_num = game.choose_correct_num game_outputs = game.outputs.where(:query_index => max_query_index) # 判断用户是否有提交 had_submmit = game_outputs.blank? ? false : true # 判断选择题是否写了标准答案 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 = has_answer.present? choose_test_cases = {:had_submmit => had_submmit, :challenge_chooses_count => challenge_chooses_count, :choose_correct_num => choose_correct_num, :test_sets => test_sets} return {:choose => chooses, :choose_test_cases => choose_test_cases, :has_answer => has_answer} end # 轮询获取状态 # resubmit是在file_update中生成的,从game_build中传入的 def game_status params, current_user Rails.logger.info("sec_key is #{params[:sec_key]}**1") game = Game.find_by_identifier(params[:identifier]) resubmit_identifier = game.resubmit_identifier # 如果没有超时并且正在评测中 # 判断评测中的状态有两种:1、如果之前没有通关的,只需判断status为1即可;如果通过关,则判断game的resubmit_identifier是否更新 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) return {:running_code_status => running_code_status, :running_code_message => running_code_message} end Rails.logger.info("sec_key is #{params[:sec_key]}**2") Rails.logger.info("##### 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) shixun = Shixun.select([:id, :status, :user_id, :test_set_permission]).find(game_challenge.shixun_id) if params[:resubmit].blank? # 非重新评测 if game_status == 2 # 通关 if shixun.status > 1 score = game.final_score # 查看答案的时候有对最终获得金币进行处理 experience = game.answer_open > 0 ? 0 : game_challenge.score.to_i 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 Rails.logger.info("sec_key is #{params[:sec_key]}**3") # 实训的最大评测次数,这个值是为了优化查询,每次只取最新的最新一次评测的结果集 max_query_index = game.query_index - 1 # 区分评测过未评测过,未评测过按需求取数据 if max_query_index > 0 qurey_test_sets = TestSet.find_by_sql("SELECT o.code, o.actual_output,o.result, o.compile_success, o.test_set_position, o.query_index,t.is_public,t.input, t.output, g.id as game_id, c.id as challenge_id FROM outputs o,games g ,challenges c,test_sets t where g.id=#{game.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 ") else qurey_test_sets = TestSet.find_by_sql("SELECT t.is_public,t.input, t.output,t.position FROM games g ,challenges c,test_sets t where g.id=#{game.id} and c.id= g.challenge_id and t.challenge_id = c.id ") end # 能进入到此处,肯定是已经返回了结果,也就是说outputs中肯定有了数据 Rails.logger.info("sec_key is #{params[:sec_key]}**4") test_sets_count = qurey_test_sets.size # had_test = Output.where(:game_id => game.id, :query_index => max_query_index) # had_test_count = had_test.count had_passed_testsests_error_count = max_query_index > 0 ? qurey_test_sets.select{|qurey_test_set| qurey_test_set.result == false}.count : 0 had_passed_testsests_error_count = test_sets_count - had_passed_testsests_error_count had_test = Output.where(:game_id => game.id, :query_index => max_query_index) error_position = had_test.blank? ? nil : had_test.select{|had_test| had_test.result == false}.last Rails.logger.info("latest output id is #{error_position.id unless error_position.blank?}") latest_output = error_position.try(:out_put).gsub(/\n/, '
').gsub(/\t/, " \; \; \; \; \; \; \; \;") if error_position.try(:out_put).present? Rails.logger.warn("last_output is #{latest_output}") # power判断用户是否有权限查看隐藏测试集(TPM管理员;平台认证的老师;花费金币查看者) power = (shixun_manager(shixun, current_user) || (current_user.is_certification_teacher)) ? 1 : (shixun.test_set_permission ? 0 : -1) # 测试集统计及处理 unless qurey_test_sets.blank? check_power = (power == 1 || game.test_sets_view) test_sets = test_set_static_data(qurey_test_sets, check_power) end # 处理生成图片类型文件 picture = (game.picture_path.nil? ? 0 : game.id) # 针对web类型的实训 web_route = game_challenge.try(:web_route) mirror_name = shixun.mirror_name Rails.logger.info("sec_key is #{params[:sec_key]}**5") # 轮询结束,更新评测耗时 e_record = EvaluateRecord.where(:identifier => params[: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 Rails.logger.warn("##game is is #{game.id}, record id is #{e_record.id}, time is**** #{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")}") # 记录前端总耗时 record = e_record.try(:consume_time) # 实训制作者当前拥有的金币 grade = User.where(:id => game.user_id).pluck(:grade).first Rails.logger.info("sec_key is #{params[:sec_key]}**6") # 高性能取上一关、下一关 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).try(:identifier) output_hash = {:test_sets => test_sets, :had_test_count => test_sets_count, :test_sets_count => test_sets_count, :had_passed_testsests_error_count => had_passed_testsests_error_count} Rails.logger.info("sec_key is #{params[:sec_key]}**7") return {:grade => grade, :gold => score, :experience => experience, :status => game_status, :had_done => had_done, :position => game_challenge.position, :port => port, :power => power, :record => record, :mirror_name => mirror_name, :picture => picture, :web_route => web_route, :latest_output => latest_output, :star => game.star, :next_game => next_game, :prev_game => prev_game}.merge(output_hash) end # 记录实训花费的时间,前端是通过ent_time - open_time,所以最终只更新open_time即可 # 总花费的时间是花费的总时间加上开启的时间(open_time) # 注意:endtime和当前时间没关系,非时间差值,必须和opentime结合使用才有意义 def cost_time params game = Game.select([:id, :cost_time, :identifier]).find_by_identifier(params[:identifier]) cost_time = params[:time].to_i game.update_attribute(:cost_time, cost_time) return{ :game => game} end def sync_modify_time params game = Game.select([:id, :modify_time, :challenge_id]).find_by_identifier(params[:identifier]) modify_time = Challenge.where(:id => game.challenge_id).pluck(:modify_time).first game.update_column(:modify_time, modify_time) return {:status => 1, :message => "success"} end # tpi弹框状态更新;true则不再显示;false每次刷新显示 def system_update params myshixun = Myshixun.find(params[:myshixun_id]) myshixun.update_attribute(:system_tip, true) return {:status => 1, :message => "success"} end # For admin # 强制重置实训 def reset_my_game params, current_user unless current_user.admin? return {:status => 403, :message => "unauthorized"} end myshixun = Myshixun.find(Game.where(:identifier => params[:identifier]).pluck(:myshixun_id).first) shixun = myshixun.shixun ActiveRecord::Base.transaction do begin g = Gitlab.client shixun_tomcat = Redmine::Configuration['shixun_tomcat'] challenges = shixun.challenges # 删除选择题用户记录 unless challenges.blank? challenges.each do |challenge| if challenge.st != 0 challenge.challenge_chooses.each do |choose| user_output = choose.choose_outputs unless user_output.blank? user_output.delete end end end end end #end myshixun_job = Base64.urlsafe_encode64("myshixun_#{myshixun.id}") StudentWork.where(:myshixun_id => myshixun.id).update_all(:myshixun_id => 0, :work_status => 0, :work_score => nil, :final_score => nil, :cost_time => 0, :update_time => nil, :compelete_status => 0, :commit_time => nil) # myshixun.destroy myshixun.delete # 主从复制出现脏读的情况 if myshixun.gpid g.delete_project(myshixun.gpid) end rescue Exception => e Rails.logger.error("myshixun reset failed #{e}") raise ActiveRecord::Rollback end end # 主从复制出现脏读的情况 sleep(1) return {:status => 1, :message => "sucess", :url => "/shixuns/#{shixun.identifier}"} end def close_webssh params, current_user myshixun_id = Game.where(:identifier => params[:identifier]).pluck(:myshixun_id).first digest = params[:identifier]+Redmine::Configuration['bridge_secret_key'] digest_key = Digest::SHA1.hexdigest("#{digest}") begin shixun_tomcat = Redmine::Configuration['shixun_tomcat'] uri = "#{shixun_tomcat}/bridge/webssh/delete" Rails.logger.info("#{current_user} => cloese_webssh digest is #{digest}") params = {:tpiID => myshixun_id, :digestKey => digest_key, :identifier => params[:identifier]} res = uri_exec uri, params if res && res['code'].to_i != 0 raise("实训云平台繁忙(繁忙等级:110)") end return {:status => 1, :message => "webssh closed"} rescue Exception => e Rails.logger.error(e) return {:status => 0, :message => "authentication failed"} end end # return: 头像,姓名,用户主页链接,学校, 点赞数, 等级(通关数) def get_passed_user params page = params[:page].to_i offset = page * 15 file_base params[:identifier] games = @game.challenge.games.where(:status => 2).includes(user: [:myshixuns, user_extensions: :school]).order("cost_time desc").offset(offset).limit(15) users = format_answer_list games {user_answer_list: users} end def get_passed_code params file_base params[:identifier] codes = @game.game_codes.select([:new_code, :path]).where("new_code is not null") code_list = [] codes.each do |code| code_list << {path: code.path, code: code.new_code} end {code_list: code_list} end def shixun_manager shixun, user_current member = shixun.shixun_members.where(:role => [1,2], :user_id => user_current.id) (!member.blank? || user_current.admin?) ? true : false end # 实训一些基础查询 def game_base identifier @game = Game.select([:myshixun_id, :status, :final_score, :answer_open, :challenge_id, :id]).find_by_identifier(identifier) @myshixun = Myshixun.select([:id, :shixun_id, :gpid, :user_id]).find(@game.myshixun_id) @shixun = Shixun.select([:status, :id]).find(@myshixun.shixun_id) end # 与文件代码相关的一些通用查询 def file_base identifier @game = Game.select([:myshixun_id, :challenge_id, :id]).find_by_identifier(identifier) @myshixun = Myshixun.select([:shixun_id, :gpid, :id, :user_id]).find(@game.myshixun_id) end private DCODES = %W(2 3 4 5 6 7 8 9 a b c f e f g h i j k l m n o p q r s t u v w x y z) # 随机生成字符 def generates_identifier(container, num) code = DCODES.sample(num).join while container.exists?(identifier: code) do code = DCODES.sample(num).join end code end def format_answer_list games user_info = [] games.each do |game| user = game.user votes = game.game_codes.first.votes passed_count = user.myshixuns.where(:status => 1).count user_info << { user_id: user.id, name: user.show_name, game_id: game.id, image_url: url_to_avatar(user), school_name: user.school_name, votes: votes, passed_count: passed_count } end user_info end end