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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +215 -13
  3. data/Rakefile +5 -0
  4. data/app/assets/config/wechat_payment_manifest.js +1 -0
  5. data/app/assets/stylesheets/wechat_payment/application.css +15 -0
  6. data/app/controllers/wechat_payment/application_controller.rb +5 -0
  7. data/app/controllers/wechat_payment/callback_controller.rb +30 -0
  8. data/app/helpers/wechat_payment/application_helper.rb +4 -0
  9. data/app/jobs/wechat_payment/application_job.rb +4 -0
  10. data/app/mailers/wechat_payment/application_mailer.rb +6 -0
  11. data/app/models/wechat_payment/application_record.rb +5 -0
  12. data/app/models/wechat_payment/payment_order.rb +228 -0
  13. data/app/models/wechat_payment/refund_order.rb +127 -0
  14. data/app/services/wechat_payment/service.rb +74 -0
  15. data/app/views/layouts/wechat_payment/application.html.erb +15 -0
  16. data/config/routes.rb +4 -0
  17. data/db/migrate/20210706075217_create_wechat_payment_payment_orders.rb +22 -0
  18. data/db/migrate/20210706095205_create_wechat_payment_refund_orders.rb +20 -0
  19. data/lib/generators/wechat_payment/goods/USAGE +8 -0
  20. data/lib/generators/wechat_payment/goods/goods_generator.rb +5 -0
  21. data/lib/generators/wechat_payment/install/USAGE +8 -0
  22. data/lib/generators/wechat_payment/install/install_generator.rb +86 -0
  23. data/lib/generators/wechat_payment/install/templates/initializer.rb +19 -0
  24. data/lib/generators/wechat_payment/routes/USAGE +8 -0
  25. data/lib/generators/wechat_payment/routes/routes_generator.rb +7 -0
  26. data/lib/tasks/wechat_payment_tasks.rake +10 -0
  27. data/lib/wechat_payment.rb +40 -2
  28. data/lib/wechat_payment/client.rb +272 -0
  29. data/lib/wechat_payment/concern/goods.rb +80 -0
  30. data/lib/wechat_payment/concern/user.rb +22 -0
  31. data/lib/wechat_payment/concern/user_goods.rb +14 -0
  32. data/lib/wechat_payment/engine.rb +7 -0
  33. data/lib/wechat_payment/invoke_result.rb +35 -0
  34. data/lib/wechat_payment/missing_key_error.rb +4 -0
  35. data/lib/wechat_payment/r_logger.rb +36 -0
  36. data/lib/wechat_payment/service_result.rb +49 -0
  37. data/lib/wechat_payment/sign.rb +41 -0
  38. data/lib/wechat_payment/version.rb +1 -1
  39. metadata +68 -9
  40. 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,7 @@
1
+ module WechatPayment
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace WechatPayment
4
+
5
+ config.autoload_paths << "#{config.root}/lib"
6
+ end
7
+ 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,4 @@
1
+
2
+ module WechatPayment
3
+ class MissingKeyError < StandardError; end
4
+ 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