init project

This commit is contained in:
Jasder
2020-03-09 00:40:16 +08:00
commit 2937b2a94d
6549 changed files with 7215173 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
class Base64ImageConverter
BASE64_HEAD = 'data:image/jpeg;base64,'.freeze
Error = Class.new(StandardError)
OutLimit = Class.new(Error)
InvalidData = Class.new(Error)
InvalidFormat = Class.new(Error)
attr_reader :opts
def initialize(**opts)
@opts = opts
end
def convert(data)
raise InvalidData, '不合法的Base64数据' unless valid_base64?(data)
io = StringIO.new(Base64.decode64(image_data data))
raise OutLimit, '文件大小超过限制' if opts[:max_size].present? && io.size > opts[:max_size]
raise InvalidFormat, '无效的格式' unless Image.new(io).image?
io
end
private
def valid_base64?(data)
data&.start_with?(BASE64_HEAD)
end
def image_data(data)
data[BASE64_HEAD.size..-1]
end
def size_limit
EduSetting.get('upload_avatar_max_size')
end
class Image
attr_reader :io
def initialize(io)
raise ArgumentError unless io.respond_to?(:read)
@io = io
end
def data
@_data ||= begin
data = io.read(9)
io.rewind
data
end
end
def image?
bitmap? || gif? || jpeg? || png?
end
def bitmap?
data[0,2] == 66.chr + 77.chr
end
def gif?
data[0,4] == 71.chr + 73.chr + 70.chr + 56.chr
end
def jpeg?
data[0,3] == 0xff.chr + 0xd8.chr + 0xff.chr
end
def png?
data[0,2] == 0x89.chr + 80.chr
end
end
end

View File

@@ -0,0 +1,8 @@
module CustomRegexp
PHONE = /1\d{10}/
EMAIL = /\A[a-zA-Z0-9]+([._\\]*[a-zA-Z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+\z/
LASTNAME = /\A[a-zA-Z0-9\u4e00-\u9fa5]+\z/
NICKNAME = /\A[\u4e00-\u9fa5_a-zA-Z0-9]+\z/
PASSWORD = /\A[a-z_A-Z0-9\-\.!@#\$%\\\^&\*\)\(\+=\{\}\[\]\/",'_<>~\·`\?:;|]{8,16}\z/
URL = /\Ahttps?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]\z/
end

View File

@@ -0,0 +1,45 @@
module CustomSortable
extend ActiveSupport::Concern
included do |base|
base.instance_variable_set("@_sort_options", {})
base.instance_variable_set("@_sort_columns", [])
base.instance_variable_set("@_sort_directions", %w(asc desc))
end
def custom_sort(relations, sort_by, sort_direction)
sort_by = self.class.sort_options[:default_by] if sort_by.blank?
sort_direction = self.class.sort_options[:default_direction] if sort_direction.blank?
return relations unless self.class.check_sort_parameter_validate(sort_by.to_s, sort_direction.to_s)
order_method = self.class.sort_options[:reorder] ? :reorder : :order
default_table = self.class.sort_options[:default_table]
if default_table.present?
relations.send(order_method, "#{default_table}.#{sort_by} #{sort_direction}")
else
relations.send(order_method, "#{sort_by} #{sort_direction}")
end
end
module ClassMethods
def sort_columns(*columns)
opts = columns.extract_options!
@_sort_options[:default_by] = opts[:default_by].to_s
@_sort_options[:default_direction] = opts[:default_direction].to_s
@_sort_options[:reorder] = opts[:reorder]
@_sort_options[:default_table] = opts[:default_table]
@_sort_columns = columns.map(&:to_s)
end
def check_sort_parameter_validate(sort_by, sort_direction)
(sort_by.blank? || @_sort_columns.include?(sort_by)) && @_sort_directions.include?(sort_direction)
end
def sort_options
@_sort_options
end
end
end

20
app/libs/gitea.rb Normal file
View File

@@ -0,0 +1,20 @@
module Gitea
class << self
def gitea_config
gitea_config = {}
begin
config = Rails.application.config_for(:configuration).symbolize_keys!
gitea_config = config[:gitea].symbolize_keys!
raise 'gitea config missing' if gitea_config.blank?
rescue => ex
raise ex if Rails.env.production?
puts %Q{\033[33m [warning] gitea config or configuration.yml missing,
please add it or execute 'cp config/configuration.yml.example config/configuration.yml' \033[0m}
gitea_config = {}
end
gitea_config
end
end
end

View File

@@ -0,0 +1,24 @@
# 基于Redis实现热门搜索关键字
class HotSearchKeyword
class << self
def add(keyword)
return if keyword.blank?
Rails.logger.info("[Hot Keyword] #{keyword} score increment ~")
Rails.cache.data.zincrby(redis_key, 1, keyword)
end
def hot(limit = 5)
Rails.cache.data.zrevrange(redis_key, 0, limit - 1)
end
def available?
Rails.cache.is_a?(ActiveSupport::Cache::RedisStore)
end
private
def redis_key
'educoder:es:hot_keyword'
end
end
end

View File

@@ -0,0 +1,2 @@
module LimitForbidControl
end

View File

@@ -0,0 +1,56 @@
module OmniAuth
module Strategies
class QQ < OmniAuth::Strategies::OAuth2
option :client_options, {
site: 'https://graph.qq.com',
authorize_url: '/oauth2.0/authorize',
token_url: '/oauth2.0/token'
}
option :token_params, { parse: :query }
def request_phase
super
end
def authorize_params
super.tap do |params|
%w[scope client_options].each do |v|
if request.params[v]
params[v.to_sym] = request.params[v]
end
end
end
end
uid do
@uid ||= begin
access_token.options[:mode] = :query
access_token.options[:param_name] = :access_token
# Response Example: "callback( {\"client_id\":\"11111\",\"openid\":\"000000FFFF\"} );\n"
response = access_token.get('/oauth2.0/me')
matched = response.body.match(/"openid":"(?<openid>\w+)"/)
matched[:openid]
end
end
info do
{
name: user_info['nickname'],
nickname: user_info['nickname'],
image: user_info['figureurl_qq_1']
}
end
extra do
{ raw_info: user_info }
end
def user_info
access_token.options[:mode] = :query
param = { oauth_consumer_key: options[:client_id], openid: uid, format: 'json' }
@user_info ||= access_token.get('/user/get_user_info', params: param, parse: :json).parsed
end
end
end
end

11
app/libs/sidebar_util.rb Normal file
View File

@@ -0,0 +1,11 @@
class SidebarUtil
class << self
def controller_name(name)
sidebar_controller_map[name]
end
def sidebar_controller_map
@_sidebar_controller_map ||= YAML.load_file(Rails.root.join('config/admins', 'sidebar.yml'))
end
end
end

83
app/libs/util.rb Normal file
View File

@@ -0,0 +1,83 @@
require 'open-uri'
module Util
module_function
def days_between(time, other_time)
raise ArgumentError if time.blank? || other_time.blank?
Date.parse(time.to_s) - Date.parse(other_time.to_s)
end
def convert_base64_image(str, **opts)
return if str.blank?
Base64ImageConverter.new(**opts).convert(str)
end
def write_file(io, path)
dir = File.dirname(path)
FileUtils.mkdir_p(dir) unless File.directory?(dir)
Rails.logger.info("### save file #{path}, size: #{io.size} ~")
File.open(path, 'wb') do |file|
if io.respond_to?(:read)
io.rewind
while buffer = io.read(8192)
file.write(buffer)
end
else
file.write(io)
end
end
end
def download_file(url, save_path)
data = open(url, &:read)
file = File.new(save_path, 'w+')
file.binmode
file << data
file.flush
file.close
file
end
def logger_error(exception)
Rails.logger.error(exception.message)
exception.backtrace.each { |message| Rails.logger.error(message) }
end
def map_or_pluck(relation, name)
relation.is_a?(Array) || relation.loaded? ? relation.map(&name.to_sym) : relation.pluck(name)
end
def extract_content(str)
return '' if str.blank?
str.gsub(/<\/?.*?>/, '').gsub(/[\n\t\r]/, '').gsub(/&nbsp;/, '')
end
def conceal(str, type = nil)
str = str.to_s
return if str.blank?
case type
when :phone then "#{str[0..2]}****#{str[-4..-1]}"
when :email then "#{str[0]}***#{str[(str.rindex('@')-1)..-1]}"
else "#{str[0..2]}***#{str[-3..-1]}"
end
end
def display_cost_time(time)
time = time.to_i
return if time.zero? || time < 60
day = time / (24 * 60 * 60)
hour = (time % (24 * 60 * 60)) / (60 * 60)
minute = (time % (60 * 60)) / 60
str = ''
str += "#{day}" unless day.zero?
str += "#{hour}小时" unless hour.zero?
str += "#{minute}" unless minute.zero?
str
end
end

View File

@@ -0,0 +1,67 @@
module Util::FileManage
module_function
# 不同的类型扩展不同的目录
def relative_path
"avatars"
end
def storage_path
File.join(Rails.root, "public", "images", relative_path)
end
def disk_filename(source_type, source_id, suffix=nil)
File.join(storage_path, "#{source_type}", "#{source_id}#{suffix}")
end
def source_disk_filename(source, suffix=nil)
disk_filename(source.class.name, source.id, suffix)
end
def exist?(source_type, source_id, suffix=nil)
File.exist?(disk_filename(source_type, source_id, suffix))
end
def exists?(source, suffix=nil)
File.exist?(disk_filename(source.class, source.id, suffix))
end
def disk_file_url(source_type, source_id, suffix = nil)
t = ctime(source_type, source_id, suffix)
File.join('/images', relative_path, "#{source_type}", "#{source_id}#{suffix}") + "?t=#{t}"
end
def source_disk_file_url(source, suffix=nil)
disk_file_url(source.class, source.id, suffix)
end
def ctime(source_type, source_id, suffix)
return nil unless exist?(source_type, source_id, suffix)
File.ctime(disk_filename(source_type, source_id, suffix)).to_i
end
def disk_auth_filename(source_type, source_id, type)
File.join(storage_path, "#{source_type}", "#{source_id}#{type}")
end
def disk_real_name_auth_filename(source_id)
disk_auth_filename('UserAuthentication', source_id, 'ID')
end
def auth_file_url(source_type, source_id, type)
disk_file_url(source_type, source_id, type)
end
def real_name_auth_file_url(source_id)
auth_file_url('UserAuthentication', source_id, 'ID')
end
def disk_professional_auth_filename(source_id)
disk_auth_filename('UserAuthentication', source_id, 'PRO')
end
def professional_auth_file_url(source_id)
auth_file_url('UserAuthentication', source_id, 'PRO')
end
end

9
app/libs/util/redis.rb Normal file
View File

@@ -0,0 +1,9 @@
module Util::Redis
class << self
def online_user_count
if Rails.cache.is_a?(ActiveSupport::Cache::RedisStore)
Rails.cache.data.scan(0, match: 'cache:_session_id:*', count: 100000).last.uniq.size
end
end
end
end

27
app/libs/util/uuid.rb Normal file
View File

@@ -0,0 +1,27 @@
module Util
module UUID
module_function
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 time_uuid(format: '%Y%m%d%H%M%S', suffix: 8)
"#{Time.zone.now.strftime(format)}#{Random.rand(10**suffix).to_i}"
end
# 随机生成字符
def generate_identifier(container, num, pre='')
code = DCODES.sample(num).join
if container == User
while container.exists?(login: pre+code) do
code = DCODES.sample(num).join
end
else
while container.exists?(identifier: code) do
code = DCODES.sample(num).join
end
end
code
end
end
end

2
app/libs/wechat.rb Normal file
View File

@@ -0,0 +1,2 @@
module Wechat
end

74
app/libs/wechat/client.rb Normal file
View File

@@ -0,0 +1,74 @@
class Wechat::Client
BASE_SITE = 'https://api.weixin.qq.com'.freeze
attr_reader :appid, :secret
def initialize(appid, secret)
@appid = appid
@secret = secret
end
def access_token
# 7200s 有效时间
Rails.cache.fetch(access_token_cache_key, expires_in: 100.minutes) do
result = request(:get, '/cgi-bin/token', appid: appid, secret: secret, grant_type: 'client_credential')
result['access_token']
end
end
def refresh_access_token
Rails.cache.delete(access_token_cache_key)
access_token
end
def jsapi_ticket
# 7200s 有效时间
Rails.cache.fetch(jsapi_ticket_cache_key, expires_in: 100.minutes) do
result = request(:get, '/cgi-bin/ticket/getticket', access_token: access_token, type: 'jsapi')
result['ticket']
end
end
def refresh_jsapi_ticket
Rails.cache.delete(jsapi_ticket_cache_key)
jsapi_ticket
end
def jscode2session(code)
request(:get, '/sns/jscode2session', appid: appid, secret: secret, js_code: code, grant_type: 'authorization_code')
end
def access_token_cache_key
"#{base_cache_key}/access_token"
end
def jsapi_ticket_cache_key
"#{base_cache_key}/jsapi_ticket"
end
def base_cache_key
"wechat/#{appid}"
end
private
def request(method, url, **params)
Rails.logger.info("[wechat] request: #{method} #{url} #{params.except(:secret).inspect}")
client = Faraday.new(url: BASE_SITE)
response = client.public_send(method, url, params)
result = JSON.parse(response.body)
Rails.logger.info("[wechat] response:#{response.status} #{result.inspect}")
if response.status != 200
raise Wechat::Error.parse(result)
end
if result['errcode'].present? && result['errcode'].to_i.nonzero?
raise Wechat::Error.parse(result)
end
result
end
end

14
app/libs/wechat/error.rb Normal file
View File

@@ -0,0 +1,14 @@
class Wechat::Error < StandardError
attr_reader :code
def initialize(code, message)
super message
@code = code
end
class << self
def parse(result)
new(result['errcode'], result['errmsg'])
end
end
end

View File

@@ -0,0 +1,17 @@
class Wechat::OfficialAccount
class << self
attr_accessor :appid, :secret
delegate :access_token, :jsapi_ticket, to: :client
def js_sdk_signature(url, noncestr, timestamp)
data = { jsapi_ticket: jsapi_ticket, noncestr: noncestr, timestamp: timestamp, url: url }
str = data.map { |k, v| "#{k}=#{v}" }.join('&')
Digest::SHA1.hexdigest(str)
end
def client
@_client ||= Wechat::Client.new(appid, secret)
end
end
end

49
app/libs/wechat/weapp.rb Normal file
View File

@@ -0,0 +1,49 @@
class Wechat::Weapp
class << self
attr_accessor :appid, :secret
delegate :access_token, :jscode2session, to: :client
def client
@_client ||= Wechat::Client.new(appid, secret)
end
def session_key(openid)
Rails.cache.read(session_key_cache_key(openid))
end
def write_session_key(openid, session_key)
Rails.cache.write(session_key_cache_key(openid), session_key)
end
def verify?(openid, str, signature)
session_key = session_key(openid)
Digest::SHA1.hexdigest("#{str}#{session_key}") == signature
end
def decrypt(session_key, encrypted_data, iv)
session_key = Base64.decode64(session_key)
encrypted_data = Base64.decode64(encrypted_data)
iv = Base64.decode64(iv)
cipher = OpenSSL::Cipher::AES.new(128, :CBC)
cipher.decrypt
cipher.padding = 0
cipher.key = session_key
cipher.iv = iv
Rails.logger.info("[Weapp] encrypted_data: #{encrypted_data}")
data = cipher.update(encrypted_data) << cipher.final
Rails.logger.info("[Weapp] data: #{data}")
result = JSON.parse(data[0...-data.last.ord])
raise Wechat::Error.new(-1, '解密错误') if result.dig('watermark', 'appid') != appid
result
end
private
def session_key_cache_key(openid)
"weapp:#{appid}:#{openid}:session_key"
end
end
end

13
app/libs/wechat_oauth.rb Normal file
View File

@@ -0,0 +1,13 @@
module WechatOauth
class << self
attr_accessor :appid, :secret, :scope, :base_url
def logger
@_logger ||= STDOUT
end
def logger=(l)
@_logger = l
end
end
end

View File

@@ -0,0 +1,14 @@
class WechatOauth::Error < StandardError
attr_reader :code
def initialize(code, msg)
super(msg)
@code = code
end
def message
I18n.t("oauth.wechat.#{code}")
rescue I18n::MissingTranslationData
super
end
end

View File

@@ -0,0 +1,61 @@
module WechatOauth::Service
module_function
def request(method, url, params)
WechatOauth.logger.info("[WechatOauth] [#{method.to_s.upcase}] #{url} || #{params}")
client = Faraday.new(url: WechatOauth.base_url)
response = client.public_send(method, url, params)
result = JSON.parse(response.body)
WechatOauth.logger.info("[WechatOauth] [#{response.status}] #{result}")
if result['errcode'].present? && result['errcode'].to_s != '0'
raise WechatOauth::Error.new(result['errcode'], result['errmsg'])
end
result
end
# https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
# response:
# {
# "access_token":"ACCESS_TOKEN",
# "expires_in":7200,
# "refresh_token":"REFRESH_TOKEN",
# "openid":"OPENID",
# "scope":"SCOPE",
# "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
# }
def access_token(code)
params = {
appid: WechatOauth.appid,
secret: WechatOauth.secret,
code: code,
grant_type: 'authorization_code'
}
request(:get, '/sns/oauth2/access_token', params)
end
# https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Authorized_Interface_Calling_UnionID.html
# response:
# {
# "openid":"OPENID",
# "nickname":"NICKNAME",
# "sex":1,
# "province":"PROVINCE",
# "city":"CITY",
# "country":"COUNTRY",
# "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
# "privilege":[
# "PRIVILEGE1",
# "PRIVILEGE2"
# ],
# "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
#
# }
def user_info(access_token, openid)
request(:get, '/sns/userinfo', access_token: access_token, openid: openid)
end
end