You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pgfqe6ch8/app/services/games_service.rb

1071 lines
52 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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?
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 || 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}
# 区分选择题和编程题st0编程题
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/, '<br/>').gsub(/\t/, "&nbsp\;&nbsp\;&nbsp\;&nbsp\;&nbsp\;&nbsp\;&nbsp\;&nbsp\;") 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>/, '<//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 => "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.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")}")
# status0表示评测成功
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 == 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
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("##### 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
# 实训的最大评测次数,这个值是为了优化查询,每次只取最新的最新一次评测的结果集
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中肯定有了数据
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/, '<br/>').gsub(/\t/, "&nbsp\;&nbsp\;&nbsp\;&nbsp\;&nbsp\;&nbsp\;&nbsp\;&nbsp\;") 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
# 轮询结束,更新评测耗时
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
# 高性能取上一关、下一关
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 => 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