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.
educoder/app/controllers/games_controller.rb

977 lines
44 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.

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