wechat_payment 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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