wechat_payment 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +215 -13
- data/Rakefile +5 -0
- data/app/assets/config/wechat_payment_manifest.js +1 -0
- data/app/assets/stylesheets/wechat_payment/application.css +15 -0
- data/app/controllers/wechat_payment/application_controller.rb +5 -0
- data/app/controllers/wechat_payment/callback_controller.rb +30 -0
- data/app/helpers/wechat_payment/application_helper.rb +4 -0
- data/app/jobs/wechat_payment/application_job.rb +4 -0
- data/app/mailers/wechat_payment/application_mailer.rb +6 -0
- data/app/models/wechat_payment/application_record.rb +5 -0
- data/app/models/wechat_payment/payment_order.rb +228 -0
- data/app/models/wechat_payment/refund_order.rb +127 -0
- data/app/services/wechat_payment/service.rb +74 -0
- data/app/views/layouts/wechat_payment/application.html.erb +15 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20210706075217_create_wechat_payment_payment_orders.rb +22 -0
- data/db/migrate/20210706095205_create_wechat_payment_refund_orders.rb +20 -0
- data/lib/generators/wechat_payment/goods/USAGE +8 -0
- data/lib/generators/wechat_payment/goods/goods_generator.rb +5 -0
- data/lib/generators/wechat_payment/install/USAGE +8 -0
- data/lib/generators/wechat_payment/install/install_generator.rb +86 -0
- data/lib/generators/wechat_payment/install/templates/initializer.rb +19 -0
- data/lib/generators/wechat_payment/routes/USAGE +8 -0
- data/lib/generators/wechat_payment/routes/routes_generator.rb +7 -0
- data/lib/tasks/wechat_payment_tasks.rake +10 -0
- data/lib/wechat_payment.rb +40 -2
- data/lib/wechat_payment/client.rb +272 -0
- data/lib/wechat_payment/concern/goods.rb +80 -0
- data/lib/wechat_payment/concern/user.rb +22 -0
- data/lib/wechat_payment/concern/user_goods.rb +14 -0
- data/lib/wechat_payment/engine.rb +7 -0
- data/lib/wechat_payment/invoke_result.rb +35 -0
- data/lib/wechat_payment/missing_key_error.rb +4 -0
- data/lib/wechat_payment/r_logger.rb +36 -0
- data/lib/wechat_payment/service_result.rb +49 -0
- data/lib/wechat_payment/sign.rb +41 -0
- data/lib/wechat_payment/version.rb +1 -1
- metadata +68 -9
- data/lib/wechat_payment/railtie.rb +0 -4
@@ -0,0 +1,272 @@
|
|
1
|
+
|
2
|
+
module WechatPayment
|
3
|
+
class Client
|
4
|
+
GATEWAY_URL = 'https://api.mch.weixin.qq.com'.freeze
|
5
|
+
|
6
|
+
def initialize # (merchant = WechatPayment)
|
7
|
+
# required_attrs = [:appid, :mch_id, :key, :app_secret, :cert_path]
|
8
|
+
# missing_attrs = required_attrs.reject { |attr| merchant.respond_to?(attr) }
|
9
|
+
# if missing_attrs.present?
|
10
|
+
# raise Exceptions::MerchantMissingAttr.new("Missing attributes: #{missing_attrs}, merchant target must respond to: appid, mch_id, key, appsecret, cert_path")
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# @merchant = merchant
|
14
|
+
# cert_path = Rails.root.join(merchant.cert_path)
|
15
|
+
#
|
16
|
+
# WechatPayment.appid = merchant.appid
|
17
|
+
# WechatPayment.key = merchant.key
|
18
|
+
# WechatPayment.mch_id = merchant.mch_id
|
19
|
+
# WechatPayment.appsecret = merchant.app_secret
|
20
|
+
# WechatPayment.set_apiclient_by_pkcs12(File.binread(cert_path), merchant.mch_id)
|
21
|
+
end
|
22
|
+
|
23
|
+
ORDER_REQUIRED_FIELD = [:out_trade_no, :spbill_create_ip, :body, :total_fee, :openid]
|
24
|
+
# 下单
|
25
|
+
def order(order_params)
|
26
|
+
check_required_key!(order_params, ORDER_REQUIRED_FIELD)
|
27
|
+
|
28
|
+
order_params.merge!(**WechatPayment.as_payment_params,
|
29
|
+
trade_type: :JSAPI,
|
30
|
+
notify_url: payment_notify_url)
|
31
|
+
|
32
|
+
# 如果是服务商模式(根据参数中有没有 sub_appid 判断),就把 openid 换成 sub_openid
|
33
|
+
if order_params[:sub_appid]
|
34
|
+
order_params[:sub_openid] = order_params.delete(:openid)
|
35
|
+
end
|
36
|
+
|
37
|
+
order_result = invoke_unifiedorder(order_params)
|
38
|
+
|
39
|
+
if order_result.success?
|
40
|
+
|
41
|
+
payment_logger.info("{params: #{order_params}, result: #{order_result}}")
|
42
|
+
# WechatPayment::ServiceResult.new(success: true, data: { order_result: order_result.with_indifferent_access, js_payload: mini_program_request_params.with_indifferent_access })
|
43
|
+
WechatPayment::ServiceResult.new(success: true, data: order_result.with_indifferent_access)
|
44
|
+
else
|
45
|
+
payment_logger.error("{params: #{order_params}, result: #{order_result}}")
|
46
|
+
WechatPayment::ServiceResult.new(success: false, errors: order_result.with_indifferent_access)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# 支付回调地址
|
51
|
+
def payment_notify_url
|
52
|
+
ENV["WECHAT_PAYMENT_NOTIFY_URL"] || "#{WechatPayment.host}/wechat_payment/callback/payment"
|
53
|
+
end
|
54
|
+
|
55
|
+
REFUND_REQUIRED_PARAMS = [:total_fee, :refund_fee, :out_trade_no, :out_refund_no]
|
56
|
+
# 退款
|
57
|
+
def refund(refund_params)
|
58
|
+
check_required_key!(refund_params, REFUND_REQUIRED_PARAMS)
|
59
|
+
|
60
|
+
refund_params.merge!(**WechatPayment.as_payment_params, notify_url: refund_notify_url)
|
61
|
+
|
62
|
+
refund_result = invoke_refund(refund_params.to_options)
|
63
|
+
|
64
|
+
if refund_result.success?
|
65
|
+
refund_logger.info "{params: #{refund_params}, result: #{refund_result}"
|
66
|
+
WechatPayment::ServiceResult.new(success: true, data: refund_result)
|
67
|
+
else
|
68
|
+
refund_logger.error "{params: #{refund_params}, result: #{refund_result}"
|
69
|
+
WechatPayment::ServiceResult.new(success: false, errors: refund_result)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# 退款回调地址
|
74
|
+
def refund_notify_url
|
75
|
+
ENV["WECHAT_REFUND_NOTIFY_URL"] || "#{WechatPayment.host}/wechat_payment/callback/refund"
|
76
|
+
end
|
77
|
+
|
78
|
+
# 处理支付回调
|
79
|
+
def self.handle_payment_notify(notify_data)
|
80
|
+
if !WechatPayment::Sign.verify?(notify_data)
|
81
|
+
payment_logger.error("{msg: 签名验证失败, errors: #{notify_data}}")
|
82
|
+
WechatPayment::ServiceResult.new(errors: notify_data, message: "回调签名验证失败")
|
83
|
+
end
|
84
|
+
|
85
|
+
result = WechatPayment::InvokeResult.new(notify_data)
|
86
|
+
|
87
|
+
if result.success?
|
88
|
+
payment_logger.info("{callback: #{notify_data}}")
|
89
|
+
WechatPayment::ServiceResult.new(success: true, data: notify_data)
|
90
|
+
else
|
91
|
+
payment_logger.error("{callback: #{notify_data}}")
|
92
|
+
WechatPayment::ServiceResult.new(errors: notify_data)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# 处理退款回调
|
97
|
+
def self.handle_refund_notify(encrypted_notify_data)
|
98
|
+
notify_data = decrypt_refund_notify(encrypted_notify_data)
|
99
|
+
|
100
|
+
result = WechatPayment::InvokeResult.new(notify_data)
|
101
|
+
if result.success?
|
102
|
+
refund_logger.info "{callback: #{notify_data}}"
|
103
|
+
WechatPayment::ServiceResult.new(success: true, data: notify_data)
|
104
|
+
else
|
105
|
+
refund_logger.error "{callback: #{notify_data}}"
|
106
|
+
WechatPayment::ServiceResult.new(errors: notify_data)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# 生成下单成功后返回给前端拉起微信支付的数据结构
|
111
|
+
def self.gen_js_pay_payload(order_result, options = {})
|
112
|
+
payment_params = {
|
113
|
+
appId: WechatPayment.sub_appid || WechatPayment.appid,
|
114
|
+
package: "prepay_id=#{order_result["prepay_id"]}",
|
115
|
+
key: options.delete(:key) || WechatPayment.key,
|
116
|
+
nonceStr: SecureRandom.hex(16),
|
117
|
+
timeStamp: Time.now.to_i.to_s,
|
118
|
+
signType: 'MD5'
|
119
|
+
}
|
120
|
+
|
121
|
+
payment_params[:paySign] = WechatPayment::Sign.generate(payment_params)
|
122
|
+
|
123
|
+
payment_params
|
124
|
+
end
|
125
|
+
|
126
|
+
GENERATE_JS_PAY_REQ_REQUIRED_FIELDS = [:prepayid, :noncestr]
|
127
|
+
def self.generate_js_pay_req(params, options = {})
|
128
|
+
check_required_options(params, GENERATE_JS_PAY_REQ_REQUIRED_FIELDS)
|
129
|
+
|
130
|
+
params = {
|
131
|
+
appId: options.delete(:appid) || WechatPayment.appid,
|
132
|
+
package: "prepay_id=#{params.delete(:prepayid)}",
|
133
|
+
key: options.delete(:key) || WechatPayment.key,
|
134
|
+
nonceStr: params.delete(:noncestr),
|
135
|
+
timeStamp: Time.now.to_i.to_s,
|
136
|
+
signType: 'MD5'
|
137
|
+
}.merge(params)
|
138
|
+
|
139
|
+
params[:paySign] = WechatPayment::Sign.generate(params)
|
140
|
+
params
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
INVOKE_UNIFIEDORDER_REQUIRED_FIELDS = [:body, :out_trade_no, :total_fee, :spbill_create_ip, :notify_url, :trade_type]
|
145
|
+
def invoke_unifiedorder(params, options = {})
|
146
|
+
params = {
|
147
|
+
appid: options.delete(:appid) || WechatPayment.appid,
|
148
|
+
mch_id: options.delete(:mch_id) || WechatPayment.mch_id,
|
149
|
+
key: options.delete(:key) || WechatPayment.key,
|
150
|
+
nonce_str: SecureRandom.uuid.tr('-', '')
|
151
|
+
}.merge(params)
|
152
|
+
|
153
|
+
check_required_options(params, INVOKE_UNIFIEDORDER_REQUIRED_FIELDS)
|
154
|
+
|
155
|
+
result = WechatPayment::InvokeResult.new(
|
156
|
+
Hash.from_xml(
|
157
|
+
invoke_remote("/pay/unifiedorder", make_payload(params), options)
|
158
|
+
)
|
159
|
+
)
|
160
|
+
|
161
|
+
yield result if block_given?
|
162
|
+
|
163
|
+
result
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
INVOKE_REFUND_REQUIRED_FIELDS = [:out_refund_no, :total_fee, :refund_fee, :op_user_id]
|
168
|
+
# out_trade_no 和 transaction_id 是二选一(必填)
|
169
|
+
def invoke_refund(params, options = {})
|
170
|
+
params = {
|
171
|
+
appid: options.delete(:appid) || WechatPayment.appid,
|
172
|
+
mch_id: options.delete(:mch_id) || WechatPayment.mch_id,
|
173
|
+
key: options.delete(:key) || WechatPayment.key,
|
174
|
+
nonce_str: SecureRandom.uuid.tr('-', ''),
|
175
|
+
}.merge(params)
|
176
|
+
|
177
|
+
params[:op_user_id] ||= params[:mch_id]
|
178
|
+
|
179
|
+
check_required_options(params, INVOKE_REFUND_REQUIRED_FIELDS)
|
180
|
+
warn("WechatPayment Warn: missing required option: out_trade_no or transaction_id must have one") if ([:out_trade_no, :transaction_id] & params.keys) == []
|
181
|
+
|
182
|
+
options = {
|
183
|
+
cert: options.delete(:apiclient_cert) || WechatPayment.apiclient_cert,
|
184
|
+
key: options.delete(:apiclient_key) || WechatPayment.apiclient_key,
|
185
|
+
verify_mode: OpenSSL::SSL::VERIFY_NONE
|
186
|
+
}.merge(options)
|
187
|
+
|
188
|
+
result = WechatPayment::InvokeResult.new(
|
189
|
+
Hash.from_xml(
|
190
|
+
invoke_remote("/secapi/pay/refund", make_payload(params), options)
|
191
|
+
)
|
192
|
+
)
|
193
|
+
|
194
|
+
yield result if block_given?
|
195
|
+
|
196
|
+
result
|
197
|
+
end
|
198
|
+
|
199
|
+
# 解密微信退款回调信息
|
200
|
+
#
|
201
|
+
# result = Hash.from_xml(request.body.read)["xml"]
|
202
|
+
#
|
203
|
+
# data = WechatPayment::Service.decrypt_refund_notify(result)
|
204
|
+
def self.decrypt_refund_notify(decrypted_data)
|
205
|
+
aes = OpenSSL::Cipher::AES.new('256-ECB')
|
206
|
+
aes.decrypt
|
207
|
+
aes.key = Digest::MD5.hexdigest(WechatPayment.key)
|
208
|
+
result = aes.update(Base64.decode64(decrypted_data)) + aes.final
|
209
|
+
Hash.from_xml(result)["root"]
|
210
|
+
end
|
211
|
+
|
212
|
+
def make_payload(params, sign_type = WechatPayment::Sign::SIGN_TYPE_MD5)
|
213
|
+
sign = WechatPayment::Sign.generate(params, sign_type)
|
214
|
+
"<xml>#{params.except(:key).sort.map { |k, v| "<#{k}>#{v}</#{k}>" }.join}<sign>#{sign}</sign></xml>"
|
215
|
+
end
|
216
|
+
|
217
|
+
def check_required_options(options, names)
|
218
|
+
names.each do |name|
|
219
|
+
warn("WechatPayment Warn: missing required option: #{name}") unless options.has_key?(name)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def invoke_remote(url, payload, options = {})
|
224
|
+
uri = URI("#{GATEWAY_URL}#{url}")
|
225
|
+
|
226
|
+
req = Net::HTTP::Post.new(uri)
|
227
|
+
req['Content-Type'] = 'application/xml'
|
228
|
+
|
229
|
+
options = {
|
230
|
+
use_ssl: true
|
231
|
+
}.merge(options)
|
232
|
+
|
233
|
+
res = Net::HTTP.start(uri.hostname, uri.port, **options) do |http|
|
234
|
+
http.use_ssl = true
|
235
|
+
http.request(req, payload)
|
236
|
+
end
|
237
|
+
|
238
|
+
res.body
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
private
|
243
|
+
|
244
|
+
# 判断 hash 是否缺少 key
|
245
|
+
def check_required_key!(data, required_keys)
|
246
|
+
lack_of_keys = required_keys - data.keys.map(&:to_sym)
|
247
|
+
|
248
|
+
if lack_of_keys.present?
|
249
|
+
raise WechatPayment::MissingKeyError.new("Parameter missing keys: #{lack_of_keys}")
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# 支付日志
|
254
|
+
def payment_logger
|
255
|
+
WechatPayment::Client.payment_logger
|
256
|
+
end
|
257
|
+
|
258
|
+
# 退款日志
|
259
|
+
def refund_logger
|
260
|
+
WechatPayment::Client.refund_logger
|
261
|
+
end
|
262
|
+
|
263
|
+
def self.payment_logger
|
264
|
+
@payment_logger ||= WechatPayment::RLogger.make("wx_payment")
|
265
|
+
end
|
266
|
+
|
267
|
+
def self.refund_logger
|
268
|
+
@refund_logger ||= WechatPayment::RLogger.make("wx_refund")
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
|
2
|
+
module WechatPayment
|
3
|
+
module Concern
|
4
|
+
module Goods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.class_eval do
|
10
|
+
cattr_accessor :user_model, :user_ref_field, :goods_ref_field, :user_goods_model, :persist_goods_attrs
|
11
|
+
|
12
|
+
self.user_model = "User"
|
13
|
+
self.user_ref_field = "user"
|
14
|
+
self.goods_ref_field = self.name.underscore
|
15
|
+
self.persist_goods_attrs = []
|
16
|
+
|
17
|
+
# 商品和用户的中间表模型,假设商品模型是 Product,那么中间模型就是 UserProduct
|
18
|
+
self.user_goods_model = "#{self.user_model}#{self.name}"
|
19
|
+
|
20
|
+
validates_numericality_of :price
|
21
|
+
validates_presence_of :name
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# 售出
|
26
|
+
# @param [User] user
|
27
|
+
# @return [WechatPaymentOrder]
|
28
|
+
def sell_to(user)
|
29
|
+
|
30
|
+
persist_goods_data = {}.tap do |h|
|
31
|
+
self.class.persist_goods_attrs.each do |attr|
|
32
|
+
h[attr] = send(attr)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
user_goods = self.class.user_goods_model.constantize.create(
|
37
|
+
self.class.goods_ref_field => self,
|
38
|
+
self.class.user_ref_field => user,
|
39
|
+
**persist_goods_data
|
40
|
+
)
|
41
|
+
|
42
|
+
user_goods.payment_orders.create(
|
43
|
+
body: name,
|
44
|
+
total_fee: price,
|
45
|
+
trade_type: :JSAPI,
|
46
|
+
customer: user
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
# 重新支付,应用场景是: 用户取消了支付后,使用最后一张订单进行支付
|
51
|
+
# @return [WechatPayment::ServiceResult]
|
52
|
+
def repay
|
53
|
+
# 如果不是待支付状态
|
54
|
+
unless pending?
|
55
|
+
WechatPayment::ServiceResult.new(message: "当前状态不可支付")
|
56
|
+
end
|
57
|
+
|
58
|
+
result = payment_orders.last.repay
|
59
|
+
|
60
|
+
if result.success?
|
61
|
+
WechatPayment::ServiceResult.new(success: true, data: result.data[:js_payload])
|
62
|
+
else
|
63
|
+
WechatPayment::ServiceResult.new(message: result.errors.first[:err_code_des])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# 退款
|
69
|
+
# @param [Integer] refund_fee
|
70
|
+
# @return [WechatPayment::ServiceResult]
|
71
|
+
def refund(refund_fee)
|
72
|
+
payment_orders.paid.last.refund(refund_fee)
|
73
|
+
end
|
74
|
+
|
75
|
+
def payment_exec_success(payment_order)
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
module WechatPayment
|
3
|
+
module Concern
|
4
|
+
module User
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
attr_accessor :spbill_create_ip
|
9
|
+
|
10
|
+
has_many :payment_orders, as: :customer, class_name: "WechatPayment::PaymentOrder"
|
11
|
+
has_many :refund_orders, as: :customer, class_name: "WechatPayment::RefundOrder"
|
12
|
+
|
13
|
+
alias me itself
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def buy(goods)
|
18
|
+
goods.sell_to(me)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
module WechatPayment
|
3
|
+
module Concern
|
4
|
+
module UserGoods
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
|
8
|
+
has_many :payment_orders, as: :goods, class_name: "WechatPayment::PaymentOrder"
|
9
|
+
has_many :refund_orders, as: :goods, class_name: "WechatPayment::PaymentOrder"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module WechatPayment
|
2
|
+
class InvokeResult < ::Hash
|
3
|
+
SUCCESS_FLAG = 'SUCCESS'.freeze
|
4
|
+
|
5
|
+
def initialize(result)
|
6
|
+
super nil # Or it will call `super result`
|
7
|
+
|
8
|
+
if result['xml'].class == Hash
|
9
|
+
result['xml'].each_pair do |k, v|
|
10
|
+
self[k] = v
|
11
|
+
end
|
12
|
+
else
|
13
|
+
result.each_pair do |k, v|
|
14
|
+
self[k] = v
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def success?
|
20
|
+
payment_success? || refund_success?
|
21
|
+
end
|
22
|
+
|
23
|
+
def payment_success?
|
24
|
+
self['return_code'] == SUCCESS_FLAG && self['result_code'] == SUCCESS_FLAG
|
25
|
+
end
|
26
|
+
|
27
|
+
def refund_success?
|
28
|
+
self['refund_status'] == SUCCESS_FLAG
|
29
|
+
end
|
30
|
+
|
31
|
+
def failure?
|
32
|
+
!success?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module WechatPayment
|
2
|
+
class RLogger
|
3
|
+
def initialize
|
4
|
+
raise Exceptions::InitializeDenied.new("please use 'ILogger.make' instead of 'ILogger.new'")
|
5
|
+
end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def make(log_file)
|
9
|
+
@logger ||= {}
|
10
|
+
|
11
|
+
log_file_name = if log_file.class.in? [String, Symbol]
|
12
|
+
log_file_name = log_file.to_sym
|
13
|
+
|
14
|
+
unless log_file_name.to_s.end_with? ".log"
|
15
|
+
log_file_name = "#{log_file_name}.log"
|
16
|
+
end
|
17
|
+
|
18
|
+
"#{root_path}/#{log_file_name}"
|
19
|
+
elsif log_file.respond_to? :to_path
|
20
|
+
log_file.to_path
|
21
|
+
else
|
22
|
+
raise Exceptions::UnsupportdParamType.new("log file parameter only support 'File' or 'String' Type.")
|
23
|
+
end
|
24
|
+
|
25
|
+
# 如果已经存在日志对象,则返回已有的日志对象
|
26
|
+
@logger[log_file_name] ||= ::Logger.new(log_file_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def root_path
|
30
|
+
@root ||= "#{Rails.root}/log"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|