diff --git a/.gitignore b/.gitignore index 98965d6da..2d915d1cd 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ /config/secrets.yml /config/redis.yml /config/elasticsearch.yml +/config/aliyun_vod.yml public/upload.html /config/configuration.yml diff --git a/app/controllers/callbacks/aliyun_vods_controller.rb b/app/controllers/callbacks/aliyun_vods_controller.rb new file mode 100644 index 000000000..cd385995d --- /dev/null +++ b/app/controllers/callbacks/aliyun_vods_controller.rb @@ -0,0 +1,24 @@ +class Callbacks::AliyunVodsController < Callbacks::BaseController + before_action :check_signature_valid! + + def create + Videos::DispatchCallbackService.call(params) + render_ok + end + + private + + def check_signature_valid! + return if AliyunVod::Sign.verify?(header_signature, header_timestamp) + + render_not_acceptable + end + + def header_timestamp + request.headers['X-VOD-TIMESTAMP'] + end + + def header_signature + request.headers['X-VOD-SIGNATURE'] + end +end \ No newline at end of file diff --git a/app/controllers/callbacks/base_controller.rb b/app/controllers/callbacks/base_controller.rb new file mode 100644 index 000000000..42e1ae972 --- /dev/null +++ b/app/controllers/callbacks/base_controller.rb @@ -0,0 +1,5 @@ +class Callbacks::BaseController < ActionController::Base + include RenderHelper + + skip_before_action :verify_authenticity_token +end \ No newline at end of file diff --git a/app/controllers/concerns/render_helper.rb b/app/controllers/concerns/render_helper.rb index 5ea73e666..0307bd4c9 100644 --- a/app/controllers/concerns/render_helper.rb +++ b/app/controllers/concerns/render_helper.rb @@ -7,6 +7,10 @@ module RenderHelper render json: { status: -1, message: message } end + def render_not_acceptable(message = '请求已拒绝') + render json: { status: 406, message: message } + end + def render_not_found(message = I18n.t('error.record_not_found')) render json: { status: 404, message: message } # render status: 404, json: { errors: errors } diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 6aaa04f12..75d515186 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -1154,6 +1154,12 @@ class CoursesController < ApplicationController #课堂的作业信息 shixun_homeworks = homeworks.search_homework_type(4) #全部实训作业 shixun_titles = shixun_homeworks.pluck(:name) + ["总得分"] + + # 更新实训作业成绩 + shixun_homeworks.includes(:homework_challenge_settings, :published_settings, :homework_commons_shixun).each do |homework| + homework.update_homework_work_score + end + shixun_homeworks = shixun_homeworks&.includes(score_student_works: :user) common_homeworks = homeworks.search_homework_type(1) #全部普通作业 diff --git a/app/controllers/homework_commons_controller.rb b/app/controllers/homework_commons_controller.rb index 59b807860..7f2519b99 100644 --- a/app/controllers/homework_commons_controller.rb +++ b/app/controllers/homework_commons_controller.rb @@ -255,29 +255,7 @@ class HomeworkCommonsController < ApplicationController def update_score tip_exception("作业还未发布,暂不能计算成绩") if @homework.publish_time.nil? || @homework.publish_time > Time.now begin - if @homework.unified_setting - student_works = @homework.student_works - user_ids = @course.students.pluck(:user_id) - else - user_ids = @course.students.where(course_group_id: @homework.published_settings.pluck(:course_group_id)).pluck(:user_id) - student_works = @homework.student_works.where(user_id: user_ids) - end - - student_works = student_works.includes(:challenge_work_scores) - - challenge_settings = @homework.homework_challenge_settings - challenge_setting_ids = challenge_settings.pluck(:challenge_id) - myshixuns = Myshixun.where(shixun_id: @homework.homework_commons_shixun&.shixun_id, user_id: user_ids).includes(:games) - myshixuns.find_each(batch_size: 100) do |myshixun| - work = student_works.select{|work| work.user_id == myshixun.user_id}.first - if work && myshixun - games = myshixun.games.select{|game| challenge_setting_ids.include?(game.challenge_id)} - HomeworksService.new.update_myshixun_work_score work, myshixun, games, @homework, challenge_settings - end - end - HomeworksService.new.update_student_eff_score @homework if (@homework.allow_late && @homework.late_time < Time.now) || - (!@homework.allow_late && @homework.end_time < Time.now) - @homework.update_attribute('calculation_time', Time.now) + @homework.update_homework_work_score normal_status("更新成功") rescue Exception => e uid_logger(e.message) diff --git a/app/controllers/users/base_controller.rb b/app/controllers/users/base_controller.rb index 969aca320..706b77802 100644 --- a/app/controllers/users/base_controller.rb +++ b/app/controllers/users/base_controller.rb @@ -21,7 +21,7 @@ class Users::BaseController < ApplicationController def private_user_resources! require_login - return if current_user.admin? || observed_logged_user? + return if current_user.admin_or_business? || observed_logged_user? render_forbidden end diff --git a/app/controllers/users/video_auths_controller.rb b/app/controllers/users/video_auths_controller.rb new file mode 100644 index 000000000..d7950f873 --- /dev/null +++ b/app/controllers/users/video_auths_controller.rb @@ -0,0 +1,26 @@ +class Users::VideoAuthsController < Users::BaseController + before_action :private_user_resources! + + def create + result = Videos::CreateAuthService.call(observed_user, create_params) + render_ok(data: result) + rescue Videos::CreateAuthService::Error => ex + render_error(ex.message) + end + + def update + video = observed_user.videos.find_by(uuid: params[:video_id]) + return render_error('该视频凭证不存在') if video.blank? + + result = AliyunVod::Service.refresh_upload_video(video.uuid) + render_ok(data: result) + rescue AliyunVod::Error => _ + render_error('刷新上传凭证失败') + end + + private + + def create_params + params.permit(:title, :file_name, :file_size, :description, :cover_url) + end +end \ No newline at end of file diff --git a/app/controllers/users/videos_controller.rb b/app/controllers/users/videos_controller.rb new file mode 100644 index 000000000..03acd76cc --- /dev/null +++ b/app/controllers/users/videos_controller.rb @@ -0,0 +1,58 @@ +class Users::VideosController < Users::BaseController + before_action :private_user_resources! + + helper_method :current_video + + def index + videos = Users::VideoQuery.call(observed_user, search_params) + + @count = videos.count + @videos = paginate videos + end + + def update + return render_error('该状态下不能编辑视频信息') unless current_video.published? + + current_video.update!(title: params[:title]) + + AliyunVod::Service.update_video_info(current_video.uuid, Title: current_video.title) rescue nil + end + + def cancel + video = observed_user.videos.find_by(uuid: params[:video_id]) + return render_not_found if video.blank? + return render_error('该状态下不能删除视频') unless video.pending? + + video.destroy! + AliyunVod::Service.delete_video([video.uuid]) rescue nil + + render_ok + end + + def review + params[:status] = 'processing' + videos = Users::VideoQuery.call(observed_user, params) + + @count = videos.count + @videos = paginate videos + end + + def batch_publish + Videos::BatchPublishService.call(observed_user, batch_publish_params) + render_ok + end + + private + + def current_video + @_current_video ||= observed_user.videos.find_by(id: params[:id]) + end + + def search_params + params.permit(:keyword, :sort_by, :sort_direction) + end + + def batch_publish_params + params.permit(videos: %i[video_id title]) + end +end \ No newline at end of file diff --git a/app/decorators/tiding_decorator.rb b/app/decorators/tiding_decorator.rb index d1f6f08e6..ab9de4b76 100644 --- a/app/decorators/tiding_decorator.rb +++ b/app/decorators/tiding_decorator.rb @@ -372,4 +372,12 @@ module TidingDecorator I18n.t(locale_format(tiding_type)) % [container.try(:title) || extra] end end + + def video_content + if tiding_type == 'System' + I18n.t(locale_format(tiding_type, status), reason: extra) % container.try(:title) + else + I18n.t(locale_format(tiding_type)) % [container.try(:title) || extra] + end + end end diff --git a/app/decorators/video_decorator.rb b/app/decorators/video_decorator.rb new file mode 100644 index 000000000..904e78dbb --- /dev/null +++ b/app/decorators/video_decorator.rb @@ -0,0 +1,5 @@ +module VideoDecorator + extend ApplicationDecorator + + display_time_method :published_at, :created_at, :updated_at +end \ No newline at end of file diff --git a/app/jobs/apply_teacher_role_join_course_notify_job.rb b/app/jobs/apply_teacher_role_join_course_notify_job.rb index c2495c036..3ae5e32db 100644 --- a/app/jobs/apply_teacher_role_join_course_notify_job.rb +++ b/app/jobs/apply_teacher_role_join_course_notify_job.rb @@ -11,7 +11,7 @@ class ApplyTeacherRoleJoinCourseNotifyJob < ApplicationJob belong_container_type tiding_type extra created_at updated_at] same_attrs = { - trigger_user_id: user.id, container_id: course.id, container_type: 'JoinCourse', + trigger_user_id: user.id, container_id: course.id, container_type: 'JoinCourse', status: 0, belong_container_id: course.id, belong_container_type: 'Course', tiding_type: 'Apply', extra: role } Tiding.bulk_insert(*attrs) do |worker| diff --git a/app/jobs/batch_publish_video_notify_job.rb b/app/jobs/batch_publish_video_notify_job.rb new file mode 100644 index 000000000..01390dc2d --- /dev/null +++ b/app/jobs/batch_publish_video_notify_job.rb @@ -0,0 +1,23 @@ +# 批量发布视频 消息任务 +class BatchPublishVideoNotifyJob < ApplicationJob + queue_as :notify + + def perform(user_id, video_ids) + user = User.find_by(id: user_id) + return if user.blank? + + attrs = %i[user_id trigger_user_id container_id container_type tiding_type status created_at updated_at] + + same_attrs = { + user_id: 1, + trigger_user_id: user.id, + container_type: 'Video', + tiding_type: 'Apply', status: 0 + } + Tiding.bulk_insert(*attrs) do |worker| + user.videos.where(id: video_ids).each do |video| + worker.add same_attrs.merge(container_id: video.id) + end + end + end +end diff --git a/app/jobs/get_aliyun_video_info_job.rb b/app/jobs/get_aliyun_video_info_job.rb new file mode 100644 index 000000000..d93186458 --- /dev/null +++ b/app/jobs/get_aliyun_video_info_job.rb @@ -0,0 +1,17 @@ +# 获取阿里云视频信息 +class GetAliyunVideoInfoJob < ApplicationJob + queue_as :default + + def perform(vod_video_id) + video = Video.find_by(uuid: vod_video_id) + return if video.blank? || video.vod_uploading? + + result = AliyunVod::Service.get_play_info(video.uuid) + cover_url = result.dig('VideoBase', 'CoverURL') + file_url = (result.dig('PlayInfoList', 'PlayInfo') || []).first&.[]('PlayURL') + + video.cover_url = cover_url if cover_url.present? && video.cover_url.blank? + video.file_url = file_url if file_url.present? + video.save! + end +end \ No newline at end of file diff --git a/app/libs/aliyun_vod.rb b/app/libs/aliyun_vod.rb new file mode 100644 index 000000000..0ce159fb6 --- /dev/null +++ b/app/libs/aliyun_vod.rb @@ -0,0 +1,5 @@ +module AliyunVod + class << self + attr_accessor :access_key_id, :access_key_secret, :base_url, :cate_id, :callback_url, :signature_key + end +end \ No newline at end of file diff --git a/app/libs/aliyun_vod/error.rb b/app/libs/aliyun_vod/error.rb new file mode 100644 index 000000000..a34785774 --- /dev/null +++ b/app/libs/aliyun_vod/error.rb @@ -0,0 +1,2 @@ +class AliyunVod::Error < StandardError +end \ No newline at end of file diff --git a/app/libs/aliyun_vod/service.rb b/app/libs/aliyun_vod/service.rb new file mode 100644 index 000000000..9350d2f43 --- /dev/null +++ b/app/libs/aliyun_vod/service.rb @@ -0,0 +1,8 @@ +module AliyunVod::Service + extend AliyunVod::Service::Base + + extend AliyunVod::Service::VideoUpload + extend AliyunVod::Service::VideoProcess + extend AliyunVod::Service::VideoManage + extend AliyunVod::Service::VideoPlay +end \ No newline at end of file diff --git a/app/libs/aliyun_vod/service/base.rb b/app/libs/aliyun_vod/service/base.rb new file mode 100644 index 000000000..85f74fc6f --- /dev/null +++ b/app/libs/aliyun_vod/service/base.rb @@ -0,0 +1,46 @@ +module AliyunVod::Service::Base + def request(method, params) + params = AliyunVod::Sign.format_params(params.compact) # 多层hash需要预先处理,保证值为string + params[:Signature] = AliyunVod::Sign.generate(params, method: method.to_s.upcase) + + Rails.logger.info("[AliyunVod] request => method: #{method}, params: #{params}") + + response = Faraday.public_send(method, AliyunVod.base_url, params) + result = JSON.parse(response.body) + + Rails.logger.info("[AliyunVod] response => status: #{response.status}, result: #{result}") + + raise AliyunVod::Error, result['Code'] if response.status != 200 + + result + rescue => ex + ::Util.logger_error(ex) + raise AliyunVod::Error, ex.message + end + + def base_params + { + AccessKeyId: AliyunVod.access_key_id, + Format: 'JSON', + Version: '2017-03-21', + SignatureMethod: 'HMAC-SHA1', + SignatureVersion: '1.0', + SignatureNonce: signature_nonce, + Timestamp: timestamp, + UserData: user_data + } + end + + def user_data + { MessageCallback: { CallbackURL: AliyunVod.callback_url } } + end + + def timestamp + Time.now.utc.iso8601 + end + + def signature_nonce + chars = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + chars.sample(16).join('') + end +end \ No newline at end of file diff --git a/app/libs/aliyun_vod/service/video_manage.rb b/app/libs/aliyun_vod/service/video_manage.rb new file mode 100644 index 000000000..703151841 --- /dev/null +++ b/app/libs/aliyun_vod/service/video_manage.rb @@ -0,0 +1,40 @@ +# 视频管理 +module AliyunVod::Service::VideoManage + # 修改视频信息 + def update_video_info(video_id, **opts) + params = { + Action: 'UpdateVideoInfo', + VideoId: video_id + }.merge(base_params) + + params = opts.merge(params) + + result = request(:post, params) + + result + end + + # 获取视频信息 + def get_video_info(video_id) + params = { + Action: 'GetVideoInfo', + VideoId: video_id + }.merge(base_params) + + result = request(:post, params) + + result + end + + # 删除视频信息 + def delete_video(video_ids) + params = { + Action: 'DeleteVideo', + VideoIds: video_ids.join(',') + }.merge(base_params) + + result = request(:post, params) + + result + end +end \ No newline at end of file diff --git a/app/libs/aliyun_vod/service/video_play.rb b/app/libs/aliyun_vod/service/video_play.rb new file mode 100644 index 000000000..0c3a42abb --- /dev/null +++ b/app/libs/aliyun_vod/service/video_play.rb @@ -0,0 +1,17 @@ +# 视频播放 +module AliyunVod::Service::VideoPlay + # 获取视频播放地址 + # https://help.aliyun.com/document_detail/56124.html?spm=a2c4g.11186623.6.715.4d7e2d52dU1CTK + def get_play_info(video_id, **opts) + params = { + Action: 'GetPlayInfo', + VideoId: video_id + }.merge(base_params) + + params = opts.merge(params) + + result = request(:post, params) + + result + end +end \ No newline at end of file diff --git a/app/libs/aliyun_vod/service/video_process.rb b/app/libs/aliyun_vod/service/video_process.rb new file mode 100644 index 000000000..eec029c20 --- /dev/null +++ b/app/libs/aliyun_vod/service/video_process.rb @@ -0,0 +1,17 @@ +# 视频处理 +module AliyunVod::Service::VideoProcess + # 提交媒体截图作业 + def submit_snapshot_job(video_id, **opts) + params = { + Action: 'SubmitSnapshotJob', + VideoId: video_id + }.merge(base_params) + params = opts.merge(params) + + result = request(:post, params) + + raise AliyunVod::Error, '提交媒体截图作业失败' if result['SnapshotJob'].blank? + + result + end +end \ No newline at end of file diff --git a/app/libs/aliyun_vod/service/video_upload.rb b/app/libs/aliyun_vod/service/video_upload.rb new file mode 100644 index 000000000..97ecd970b --- /dev/null +++ b/app/libs/aliyun_vod/service/video_upload.rb @@ -0,0 +1,37 @@ +# 视频上传 +module AliyunVod::Service::VideoUpload + # 获取视频上传地址和凭证 + def create_upload_video(title, filename, **opts) + params = { + Action: 'CreateUploadVideo', + Title: title, + FileName: filename + }.merge(base_params) + + # 分类 + cate_id = AliyunVod.cate_id + params[:CateId] = cate_id if cate_id.present? + + params = opts.merge(params) + + result = request(:post, params) + + raise AliyunVod::Error, '获取上传凭证失败' if result['UploadAddress'].blank? + + result + end + + # 刷新视频上传凭证 + def refresh_upload_video(video_id) + params = { + Action: 'RefreshUploadVideo', + VideoId: video_id + }.merge(base_params) + + result = request(:post, params) + + raise AliyunVod::Error, '刷新上传凭证失败' if result['UploadAddress'].blank? + + result + end +end \ No newline at end of file diff --git a/app/libs/aliyun_vod/sign.rb b/app/libs/aliyun_vod/sign.rb new file mode 100644 index 000000000..28f100acc --- /dev/null +++ b/app/libs/aliyun_vod/sign.rb @@ -0,0 +1,41 @@ +module AliyunVod::Sign + # https://help.aliyun.com/document_detail/44434.html?spm=a2c4g.11186623.2.16.354c7853oqlhMb&/#SignatureNonce + def self.generate(params, **opts) + method = opts[:method] || 'POST' + key = opts[:key] || AliyunVod.access_key_secret + '&' + digest = OpenSSL::Digest.new('sha1') + + str = params_to_string(params) + str = percent_encode(str) + str = "#{method}&%2F&#{str}" + + Base64.encode64(OpenSSL::HMAC.digest(digest, key, str)).gsub(/\n/, '') + end + + def self.verify?(signature, timestamp) + content = "#{AliyunVod.callback_url}|#{timestamp}|#{AliyunVod.signature_key}" + our_signature = Digest::MD5.hexdigest(content) + ActiveSupport::SecurityUtils.secure_compare(signature, our_signature) + end + + def self.params_to_string(params) + params.sort.map { |k, v| "#{percent_encode(k)}=#{percent_encode(v)}" }.join('&') + end + + def self.percent_encode(str) + return '' if str.blank? + CGI::escape(str.to_s).gsub(/\+/,'%20').gsub(/\*/,'%2A').gsub(/%7E/,'~') + end + + def self.format_params(params) + params.each_with_object({}) do |arr, obj| + obj[arr[0]] = arr[1].is_a?(Hash) ? parse_hash_to_str(arr[1]) : arr[1] + end + end + + def self.parse_hash_to_str(hash) + hash.each_with_object({}) do |h, obj| + obj[h[0]] = h[1].is_a?(Hash) ? parse_hash_to_str(h[1].clone) : h[1].to_s + end.to_json + end +end \ No newline at end of file diff --git a/app/models/bidding_user.rb b/app/models/bidding_user.rb index b518fb45e..3a427a011 100644 --- a/app/models/bidding_user.rb +++ b/app/models/bidding_user.rb @@ -5,7 +5,7 @@ class BiddingUser < ApplicationRecord belongs_to :project_package, counter_cache: true aasm(:status) do - state :pending, initiali: true + state :pending, initial: true state :bidding_won state :bidding_lost diff --git a/app/models/course.rb b/app/models/course.rb index 1f38827ae..25a017faa 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -327,7 +327,7 @@ class Course < ApplicationRecord #创建课程后,给该用户发送消息 def send_tiding - self.tidings << Tiding.new(user_id: tea_id, trigger_user_id: tea_id, belong_container_id: id, + self.tidings << Tiding.new(user_id: tea_id, trigger_user_id: 1, belong_container_id: id, belong_container_type: 'Course', tiding_type: 'System') end diff --git a/app/models/course_message.rb b/app/models/course_message.rb index 42cbe00c8..016479425 100644 --- a/app/models/course_message.rb +++ b/app/models/course_message.rb @@ -28,7 +28,7 @@ class CourseMessage < ApplicationRecord def send_deal_tiding deal_status # 发送申请处理结果消息 Tiding.create!( - user_id: course_message_id, trigger_user: User.current, container_id: course_id, container_type: 'DealCourse', + user_id: course_message_id, trigger_user: 1, container_id: course_id, container_type: 'DealCourse', belong_container: course, extra: content.to_i == 2 ? '9' : '7', tiding_type: 'System', status: deal_status ) # 将申请消息置为已处理 diff --git a/app/models/homework_common.rb b/app/models/homework_common.rb index 9aa4a8fe1..112fc523b 100644 --- a/app/models/homework_common.rb +++ b/app/models/homework_common.rb @@ -264,4 +264,30 @@ class HomeworkCommon < ApplicationRecord def challenge_score challenge_id homework_challenge_settings.find_by(challenge_id: challenge_id)&.score.to_f end + + def update_homework_work_score + if unified_setting + works = student_works + user_ids = course.students.pluck(:user_id) + else + user_ids = course.students.where(course_group_id: published_settings.pluck(:course_group_id)).pluck(:user_id) + works = student_works.where(user_id: user_ids) + end + + works = works.includes(:challenge_work_scores) + + challenge_settings = homework_challenge_settings + challenge_setting_ids = challenge_settings.pluck(:challenge_id) + myshixuns = Myshixun.where(shixun_id: homework_commons_shixun&.shixun_id, user_id: user_ids).includes(:games) + myshixuns.find_each(batch_size: 100) do |myshixun| + work = works.select{|work| work.user_id == myshixun.user_id}.first + if work && myshixun + games = myshixun.games.select{|game| challenge_setting_ids.include?(game.challenge_id)} + HomeworksService.new.update_myshixun_work_score work, myshixun, games, self, challenge_settings + end + end + HomeworksService.new.update_student_eff_score(self) if (allow_late && late_time < Time.now) || + (!allow_late && end_time < Time.now) + update_attribute('calculation_time', Time.now) + end end diff --git a/app/models/library.rb b/app/models/library.rb index 3c7222140..e2bc20989 100644 --- a/app/models/library.rb +++ b/app/models/library.rb @@ -16,7 +16,7 @@ class Library < ApplicationRecord validates :uuid, presence: true, uniqueness: true aasm(:status) do - state :pending, initiali: true + state :pending, initial: true state :processing state :refused state :published diff --git a/app/models/library_apply.rb b/app/models/library_apply.rb index 13e195f34..0a7ec8aec 100644 --- a/app/models/library_apply.rb +++ b/app/models/library_apply.rb @@ -4,7 +4,7 @@ class LibraryApply < ApplicationRecord belongs_to :library aasm(:status) do - state :pending, initiali: true + state :pending, initial: true state :refused state :agreed diff --git a/app/models/project_package.rb b/app/models/project_package.rb index 219f60ca4..a8e134918 100644 --- a/app/models/project_package.rb +++ b/app/models/project_package.rb @@ -17,7 +17,7 @@ class ProjectPackage < ApplicationRecord scope :invisible, -> { where(status: %i[pending applying refused]) } aasm(:status) do - state :pending, initiali: true + state :pending, initial: true state :applying state :refused state :published diff --git a/app/models/project_package_apply.rb b/app/models/project_package_apply.rb index 5116f075f..0dd69c0cf 100644 --- a/app/models/project_package_apply.rb +++ b/app/models/project_package_apply.rb @@ -4,7 +4,7 @@ class ProjectPackageApply < ApplicationRecord belongs_to :project_package aasm(:status) do - state :pending, initiali: true + state :pending, initial: true state :refused state :agreed diff --git a/app/models/student_work.rb b/app/models/student_work.rb index 9151ca501..074068273 100644 --- a/app/models/student_work.rb +++ b/app/models/student_work.rb @@ -123,10 +123,10 @@ class StudentWork < ApplicationRecord # 更新作品成绩 def set_work_score - if work_status > 0 && homework_common && homework_common.homework_detail_manual && !self.ultimate_score + if work_status > 0 && homework_common && !self.ultimate_score case homework_common.homework_type when "normal", "group" - if !homework_common.homework_detail_manual.final_mode + if !homework_common&.homework_detail_manual&.final_mode tea_ass_proportion = homework_common.homework_detail_manual.ta_proportion tea_proportion = homework_common.homework_detail_manual.te_proportion if self.teacher_score diff --git a/app/models/user.rb b/app/models/user.rb index e999ebdd8..7f724d081 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -60,8 +60,8 @@ class User < ApplicationRecord has_many :games, :dependent => :destroy has_many :created_subjects, foreign_key: :user_id, class_name: 'Subject' - has_many :subjects, :through => :subject_members has_many :subject_members, :dependent => :destroy + has_many :subjects, :through => :subject_members has_many :grades, :dependent => :destroy has_many :experiences, :dependent => :destroy has_many :student_works, :dependent => :destroy @@ -135,6 +135,8 @@ class User < ApplicationRecord # 教学案例 has_many :libraries, dependent: :destroy + # 视频 + has_many :videos, dependent: :destroy # Groups and active users scope :active, lambda { where(status: STATUS_ACTIVE) } diff --git a/app/models/video.rb b/app/models/video.rb new file mode 100644 index 000000000..381db58a9 --- /dev/null +++ b/app/models/video.rb @@ -0,0 +1,36 @@ +class Video < ApplicationRecord + include AASM + + belongs_to :user + + has_many :video_applies, dependent: :destroy + has_one :processing_video_apply, -> { where(status: :pending) }, class_name: 'VideoApply' + + aasm(:status) do + state :pending, initial: true + state :processing + state :refused + state :published + + event :apply_publish do + transitions from: :pending, to: :processing + end + + event :refuse do + transitions from: :processing, to: :refused + end + + event :publish do + transitions from: :processing, to: :published, guard: :vod_uploaded? + end + end + + aasm(:vod_status, namespace: :vod) do + state :uploading, initial: true + state :uploaded + + event :upload_success do + transitions from: :uploading, to: :uploaded + end + end +end \ No newline at end of file diff --git a/app/models/video_apply.rb b/app/models/video_apply.rb new file mode 100644 index 000000000..694a448d7 --- /dev/null +++ b/app/models/video_apply.rb @@ -0,0 +1,19 @@ +class VideoApply < ApplicationRecord + include AASM + + belongs_to :video + + aasm(:status) do + state :pending, initial: true + state :refused + state :agreed + + event :refuse do + transitions from: :pending, to: :refused + end + + event :agree do + transitions from: :pending, to: :agreed + end + end +end \ No newline at end of file diff --git a/app/queries/application_query.rb b/app/queries/application_query.rb new file mode 100644 index 000000000..3a92cc6e8 --- /dev/null +++ b/app/queries/application_query.rb @@ -0,0 +1,3 @@ +class ApplicationQuery + include Callable +end \ No newline at end of file diff --git a/app/queries/users/video_query.rb b/app/queries/users/video_query.rb new file mode 100644 index 000000000..cfaa314cc --- /dev/null +++ b/app/queries/users/video_query.rb @@ -0,0 +1,28 @@ +class Users::VideoQuery < ApplicationQuery + include CustomSortable + + sort_columns :published_at, :title, default_by: :published_at, default_direction: :desc + + attr_reader :user, :params + + def initialize(user, params) + @user = user + @params = params + end + + def call + videos = user.videos + + videos = + case params[:status] + when 'published' then videos.published + when 'processing' then videos.processing + else videos.published + end + + keyword = params[:keyword].to_s.strip + videos = videos.where('title LIKE ?', "%#{keyword}%") if keyword.present? + + custom_sort(videos, params[:sort_by], params[:sort_direction]) + end +end \ No newline at end of file diff --git a/app/services/concerns/elasticsearch_able.rb b/app/services/concerns/elasticsearch_able.rb index eb7d4de04..80ca5467e 100644 --- a/app/services/concerns/elasticsearch_able.rb +++ b/app/services/concerns/elasticsearch_able.rb @@ -22,14 +22,7 @@ module ElasticsearchAble fragment_size: EduSetting.get('es_highlight_fragment_size') || 30, tag: '', fields: { - name: { type: 'plain' }, - challenge_names: { type: 'plain' }, - challenge_tag_names: { type: 'plain' }, - description: { type: 'plain' }, - subject_stages: { type: 'plain' }, - content: { type: 'plain' }, - descendants_contents: { type: 'plain' }, - member_user_names: { type: 'plain' } + '*' => { type: 'plain', number_of_fragments: 3 } } } end @@ -48,4 +41,4 @@ module ElasticsearchAble def page params[:page].to_i <= 0 ? 1 : params[:page].to_i end -end \ No newline at end of file +end diff --git a/app/services/videos/agree_apply_service.rb b/app/services/videos/agree_apply_service.rb new file mode 100644 index 000000000..50791935a --- /dev/null +++ b/app/services/videos/agree_apply_service.rb @@ -0,0 +1,35 @@ +class Videos::AgreeApplyService < ApplicationService + Error = Class.new(StandardError) + + attr_reader :video_apply, :video, :user + + def initialize(video_apply, user) + @video_apply = video_apply + @video = video_apply.video + @user = user + end + + def call + raise Error, '该状态下不能进行此操作' unless video_apply.may_agree? && video.may_publish? + + ActiveRecord::Base.transaction do + video_apply.agree! + + video.published_at = Time.now + video.publish + video.save! + + # 将消息改为已处理 + Tiding.where(container_id: video.id, container_type: 'Video', tiding_type: 'Apply', status: 0).update_all(status: 1) + notify_video_author! + end + end + + private + + def notify_video_author! + Tiding.create!(user_id: video.user_id, trigger_user_id: 1, + container_id: video.id, container_type: 'Video', + tiding_type: 'System', status: 1) + end +end \ No newline at end of file diff --git a/app/services/videos/batch_publish_service.rb b/app/services/videos/batch_publish_service.rb new file mode 100644 index 000000000..3ff16dc57 --- /dev/null +++ b/app/services/videos/batch_publish_service.rb @@ -0,0 +1,31 @@ +class Videos::BatchPublishService < ApplicationService + attr_reader :user, :params + + def initialize(user, params) + @user = user + @params = params + end + + def call + video_params = Array.wrap(params[:videos]).compact + return if video_params.blank? + + video_ids = [] + ActiveRecord::Base.transaction do + video_params.each do |param| + video = user.videos.find_by(uuid: param[:video_id]) + next if video.blank? || video.processing_video_apply.present? + + video.title = param[:title].to_s.strip.presence || video.title + video.apply_publish + video.save! + + video.video_applies.create! + + video_ids << video.id + end + end + + BatchPublishVideoNotifyJob.perform_later(user.id, video_ids) if video_ids.present? + end +end \ No newline at end of file diff --git a/app/services/videos/create_auth_service.rb b/app/services/videos/create_auth_service.rb new file mode 100644 index 000000000..8d83ca4e2 --- /dev/null +++ b/app/services/videos/create_auth_service.rb @@ -0,0 +1,41 @@ +class Videos::CreateAuthService < ApplicationService + Error = Class.new(StandardError) + + attr_reader :user, :params + + def initialize(user, params) + @user = user + @params = params.clone + end + + def call + validate! + + result = upload_video_result + + Video.create!(user: user, uuid: result['VideoId'], title: title, cover_url: params[:cover_url]) + + result + end + + private + + def title + @_title ||= params.delete(:title).to_s.strip + end + + def filename + @_filename ||= params.delete(:file_name).to_s.strip + end + + def validate! + raise Error, '视频标题不能为空' if title.blank? + raise Error, '源文件名不能为空' if filename.blank? + end + + def upload_video_result + AliyunVod::Service.create_upload_video(title, filename, params) + rescue AliyunVod::Error => _ + raise Error, '获取视频上传凭证失败' + end +end \ No newline at end of file diff --git a/app/services/videos/dispatch_callback_service.rb b/app/services/videos/dispatch_callback_service.rb new file mode 100644 index 000000000..3bdabd91a --- /dev/null +++ b/app/services/videos/dispatch_callback_service.rb @@ -0,0 +1,27 @@ +class Videos::DispatchCallbackService < ApplicationService + attr_reader :video, :params + + def initialize(params) + @video = Video.find_by(uuid: params[:VideoId]) + @params = params + end + + def call + return if video.blank? + + # TODO:: 拆分事件分发 + case params['EventType'] + when 'FileUploadComplete' then # 视频上传完成 + video.file_url = params['FileUrl'] + video.upload_success + video.save! + when 'SnapshotComplete' then # 封面截图完成 + return if video.cover_url.present? + + video.update!(cover_url: params['CoverUrl']) + end + + rescue => ex + Util.logger_error(ex) + end +end \ No newline at end of file diff --git a/app/services/videos/refuse_apply_service.rb b/app/services/videos/refuse_apply_service.rb new file mode 100644 index 000000000..5d796db79 --- /dev/null +++ b/app/services/videos/refuse_apply_service.rb @@ -0,0 +1,38 @@ +class Videos::RefuseApplyService < ApplicationService + Error = Class.new(StandardError) + + attr_reader :video_apply, :video, :user, :params + + def initialize(video_apply, user, params) + @video_apply = video_apply + @video = video_apply.video + @user = user + @params = params + end + + def call + reason = params[:reason].to_s.strip + raise Error, '原因不能为空' if reason.blank? + raise Error, '该状态下不能进行此操作' unless video_apply.may_refuse? + + ActiveRecord::Base.transaction do + video_apply.reason = reason + video_apply.refuse + video_apply.save! + + video.refuse! + + # 将消息改为已处理 + Tiding.where(container_id: video.id, container_type: 'Video', tiding_type: 'Apply', status: 0).update_all(status: 1) + notify_video_author! + end + end + + private + + def notify_video_author! + Tiding.create!(user_id: video.user_id, trigger_user_id: 1, + container_id: video.id, container_type: 'Video', + tiding_type: 'System', status: 2, extra: video_apply.reason) + end +end \ No newline at end of file diff --git a/app/views/users/videos/_video.json.jbuilder b/app/views/users/videos/_video.json.jbuilder new file mode 100644 index 000000000..9b91c5e94 --- /dev/null +++ b/app/views/users/videos/_video.json.jbuilder @@ -0,0 +1,5 @@ +json.extract! video, :id, :title, :cover_url, :file_url + +json.published_at video.display_published_at +json.created_at video.display_created_at +json.updated_at video.display_updated_at \ No newline at end of file diff --git a/app/views/users/videos/index.json.jbuilder b/app/views/users/videos/index.json.jbuilder new file mode 100644 index 000000000..f4a34aa00 --- /dev/null +++ b/app/views/users/videos/index.json.jbuilder @@ -0,0 +1,2 @@ +json.count @count +json.videos @videos, partial: 'video', as: :video \ No newline at end of file diff --git a/app/views/users/videos/review.json.jbuilder b/app/views/users/videos/review.json.jbuilder new file mode 100644 index 000000000..3e0fe8b93 --- /dev/null +++ b/app/views/users/videos/review.json.jbuilder @@ -0,0 +1,7 @@ +json.count @count +json.videos do + json.array! @videos.each do |video| + json.partial! 'video', video: video + json.file_url nil + end +end \ No newline at end of file diff --git a/app/views/users/videos/update.json.jbuilder b/app/views/users/videos/update.json.jbuilder new file mode 100644 index 000000000..ed69a174c --- /dev/null +++ b/app/views/users/videos/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'video', video: current_video \ No newline at end of file diff --git a/config/aliyun_vod.yml.example b/config/aliyun_vod.yml.example new file mode 100644 index 000000000..aa6547a62 --- /dev/null +++ b/config/aliyun_vod.yml.example @@ -0,0 +1,16 @@ +defaults: &defaults + access_key_id: 'test' + access_key_secret: 'test' + base_url: 'http://vod.cn-shanghai.aliyuncs.com' + cate_id: '-1' + callback_url: 'http://47.96.87.25:48080/api/callbacks/aliyun_vod.json' + signature_key: 'test12345678' + +development: + <<: *defaults + +test: + <<: *defaults + +production: + <<: *defaults \ No newline at end of file diff --git a/config/initializers/aliyun_vod_init.rb b/config/initializers/aliyun_vod_init.rb new file mode 100644 index 000000000..d133fffbf --- /dev/null +++ b/config/initializers/aliyun_vod_init.rb @@ -0,0 +1,7 @@ +config = Rails.application.config_for(:aliyun_vod) +AliyunVod.access_key_id = config['access_key_id'] +AliyunVod.access_key_secret = config['access_key_secret'] +AliyunVod.base_url = config['base_url'] || 'http://vod.cn-shanghai.aliyuncs.com'.freeze +AliyunVod.cate_id = config['cate_id'] +AliyunVod.callback_url = config['callback_url'] +AliyunVod.signature_key = config['signature_key'] diff --git a/config/locales/tidings/zh-CN.yml b/config/locales/tidings/zh-CN.yml index d00787a7f..a4d213161 100644 --- a/config/locales/tidings/zh-CN.yml +++ b/config/locales/tidings/zh-CN.yml @@ -220,3 +220,8 @@ BiddingEnd_end: "你发布的众包任务:%s,已进入选标阶段,请尽快进行选择确认!" BiddingWon_end: "恭喜,你应征的众包任务:%s,在评选环节中标了" BiddingLost_end: "很遗憾,你应征投稿的众包任务:%s,未中标" + Video: + Apply_end: "申请发布视频:%s" + System: + 1_end: "你提交的发布视频申请:%s,审核已通过" + 2_end: "你提交的发布视频申请:%s,审核未通过
原因:%{reason}" diff --git a/config/routes.rb b/config/routes.rb index b8db3afa6..9e115c3a9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -70,6 +70,16 @@ Rails.application.routes.draw do resources :recent_contacts, only: [:index] resource :private_message_details, only: [:show] resource :unread_message_info, only: [:show] + + # 视频 + resources :videos, only: [:index, :update] do + collection do + get :review + post :batch_publish + post :cancel + end + end + resource :video_auths, only: [:create, :update] end @@ -718,6 +728,8 @@ Rails.application.routes.draw do scope module: :projects do resources :project_applies, only: [:create] end + + post 'callbacks/aliyun_vod', to: 'callbacks/aliyun_vods#create' end #git 认证回调 diff --git a/db/migrate/20190808025851_create_videos.rb b/db/migrate/20190808025851_create_videos.rb new file mode 100644 index 000000000..e7d89f7eb --- /dev/null +++ b/db/migrate/20190808025851_create_videos.rb @@ -0,0 +1,20 @@ +class CreateVideos < ActiveRecord::Migration[5.2] + def change + create_table :videos do |t| + t.references :user, index: true, type: :integer + t.string :title + t.string :uuid, comment: 'Aliyun媒体ID' + t.string :cover_url, comment: '视频封面' + t.string :file_url, comment: '视频地址' + + t.string :status + t.string :vod_status, comment: 'Aliyun媒体状态' + + t.datetime :published_at, index: true + + t.timestamps + + t.index :uuid, unique: true + end + end +end diff --git a/db/migrate/20190809010636_create_video_applies.rb b/db/migrate/20190809010636_create_video_applies.rb new file mode 100644 index 000000000..e8b4037c5 --- /dev/null +++ b/db/migrate/20190809010636_create_video_applies.rb @@ -0,0 +1,10 @@ +class CreateVideoApplies < ActiveRecord::Migration[5.2] + def change + create_table :video_applies do |t| + t.references :video, index: true, type: :integer + t.string :status + t.string :reason + t.timestamps + end + end +end diff --git a/public/react/src/modules/courses/coursesDetail/CoursesBanner.js b/public/react/src/modules/courses/coursesDetail/CoursesBanner.js index 2ab4228bb..a438e78a8 100644 --- a/public/react/src/modules/courses/coursesDetail/CoursesBanner.js +++ b/public/react/src/modules/courses/coursesDetail/CoursesBanner.js @@ -580,7 +580,7 @@ class CoursesBanner extends Component {
  • - 邀请码 + 邀请码 {coursedata.code_halt === true? "已停用" : coursedata.invite_code} diff --git a/public/react/src/modules/courses/css/Courses.css b/public/react/src/modules/courses/css/Courses.css index b6388b1e7..7fae8115f 100644 --- a/public/react/src/modules/courses/css/Courses.css +++ b/public/react/src/modules/courses/css/Courses.css @@ -1649,3 +1649,17 @@ input.ant-input-number-input:focus { left: -95px; top:120px; } + +/*.yslnobinput input{*/ +/* width: 280px;*/ +/* height: 40px !important;*/ +/* background-color: #fafafa!important;*/ +/*}*/ +/*.yslnobinput .ant-input-number-input-wrap{*/ +/* width: 280px;*/ +/* height: 40px !important;*/ +/* background-color: #fafafa!important;*/ +/*}*/ +.yslinputcourput .ant-form-explain{ + padding-left: 0px !important; +} \ No newline at end of file diff --git a/public/react/src/modules/courses/exercise/new/JudgeDisplay.js b/public/react/src/modules/courses/exercise/new/JudgeDisplay.js index 60fbd5ac2..a6996ed84 100644 --- a/public/react/src/modules/courses/exercise/new/JudgeDisplay.js +++ b/public/react/src/modules/courses/exercise/new/JudgeDisplay.js @@ -85,10 +85,10 @@ class JudgeDisplay extends Component{ { question_choices.map((item, optionIndex) => { // 单选 return ( -
    +
    {/* {item.choice_text} */} - {/* */}
    { - standard_answer.map((answers, index) => { - return
    - 答案(填空{index+1}): + standard_answer.map((answers, _index) => { + return
    + 答案(填空{_index+1}):
    { answers.answer_text.map((item, itemIndex) => { return })} {/* {item} */} diff --git a/public/react/src/modules/courses/new/CoursesNew.js b/public/react/src/modules/courses/new/CoursesNew.js index fc4719013..86789b8a2 100644 --- a/public/react/src/modules/courses/new/CoursesNew.js +++ b/public/react/src/modules/courses/new/CoursesNew.js @@ -1,5 +1,5 @@ import React, {Component} from "React"; -import {Form, Select, Input, Button, Checkbox, DatePicker,Spin,Icon,AutoComplete} from "antd"; +import {Form, Select, Input, Button, Checkbox, DatePicker,Spin,Icon,AutoComplete,InputNumber} from "antd"; import ApplyForAddOrgModal from '../../user/modal/ApplyForAddOrgModal'; import axios from 'axios'; import "../css/Courses.css"; @@ -539,7 +539,7 @@ class CoursesNew extends Component {
    -
    +