# 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.admin? || current_user.id == shixun.try(:user_id) || current_user.is_certification_teacher)
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 = shixun.discusses.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 current_user.is_certification_teacher || shixun_manager(shixun, current_user) || game.status == 2
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 && shixun.test_set_permission)
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}
# 区分选择题和编程题,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) # 新结构脚本是实时传送的,所以不会影响
# 区分评测过未评测过,未评测过按需求取数据
if max_query_index > 0
qurey_test_sets = TestSet.find_by_sql("SELECT o.actual_output, o.out_put, o.result, 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
# 测试集统计及处理
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|
actual_output = test_set.attributes.count > 4 ? test_set.try(:actual_output) : nil
result = test_set.attributes.count > 4 ? test_set.try(:result) : nil
#actual_output = Base64.encode64(actual_output)
compile_success = (actual_output.blank? || actual_output.downcase.match(/error/).present?) ? 0 : 1
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>/, '/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.actual_output, o.out_put, o.result, 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
record = EvaluateRecord.create!(:user_id => current_user.id, :shixun_id => @myshixun.shixun.id, :game_id => @game.id)
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 => "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}
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))
# 更新时间是为了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)}
# 评测有文件输出的需要特殊传字段 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))
# 选择题如果通关了,则不让再评测
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
# 查看答案的时候处理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
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)))
return
end
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 : 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
# 实训的最大评测次数,这个值是为了优化查询,每次只取最新的最新一次评测的结果集
max_query_index = game.query_index - 1
# 区分评测过未评测过,未评测过按需求取数据
if max_query_index > 0
qurey_test_sets = TestSet.find_by_sql("SELECT o.actual_output,o.result, 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中肯定有了数据
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 && shixun.test_set_permission)) ? 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
# 轮询结束,更新评测耗时
e_record = EvaluateRecord.where(:game_id => game.id).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
# 高性能取上一关、下一关
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}
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 => nil, :work_status => 0)
# 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
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