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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7c25024b1f10c62e7ddd1d03489d9d6464f51f1e0540a981574685099ed0c9e
4
- data.tar.gz: ba71d84612d41cdde00a24994237b2d9339d47689e61005695bbb0a15c5f4dda
3
+ metadata.gz: 2a2e445d0e1ce0d2cc83eef761a18bbd90ade6264e2798e2ce0a1bc1756b9139
4
+ data.tar.gz: bcfbaf1b877b6a7656cb378a894293c593e96862b018c9415eac13ed00063961
5
5
  SHA512:
6
- metadata.gz: '090abcf0b7ffacad8b5dd22bf5cd0d82938b68a6b4d156ffda335e3672f2c3136e8bf63fa1961d465d74561c951bf86876836154c11ef69d1577e18f46d439f7'
7
- data.tar.gz: 5c391978e493f2ca85f61595c758bb209d14f2700205150d5156e65e88b230fadd1ebfd4899f071a53ed415b314cc02d4ec0826a09183c036aac53150acc0aca
6
+ metadata.gz: e3157ecac086e7f9d491d1359f4ce3de8208068f277a041c9a57d9b20abd6406d1648fce5d11c25f6cbc9d8f6aea64b24e2477383c73a775212b2ac11d42bf2c
7
+ data.tar.gz: 3e41d27212801e96e9c359cb60a6a51f6c7e63cd4dbcbcd3deaba2c0ee72c13a55423e538422b1ff952602ef05717cb5df3c6d0a75dda131c6f3befff9aa6dcd
data/README.md CHANGED
@@ -1,28 +1,230 @@
1
1
  # WechatPayment
2
- Short description and motivation.
2
+ 微信支付的 engine
3
3
 
4
- ## Usage
5
- How to use my plugin.
6
4
 
7
- ## Installation
8
- Add this line to your application's Gemfile:
5
+ ## Convention
6
+ 在使用该 Engine 之前,需要先建立`用户模型`,`商品模型`,`用户商品关联模型`,用户表需要有 open_id 字段,
7
+ 商品表需要有 price 和 name 字段。假设`商品模型`为`Product`,则用户和商品之间的关联模型为`UserProduct`,
8
+ 关联模型属于`user`和`product`。
9
+
10
+ ## Getting Started
11
+ 1. 在`Gemfile`中添加
12
+ ```ruby
13
+ gem 'wechat_payment'
14
+ ```
15
+
16
+ 2. 执行 `bundle install`
17
+
18
+ 3. 初始化
19
+ ```bash
20
+ # rails g wechat_payment:install [GoodsModel] [UserModel]
21
+ $ rails g wechat_payment:install Product User
22
+ $ rails db:migrate
23
+ ```
24
+
25
+ ## 配置
26
+ 打开 `config/initializers/wechat_payment.rb`,將里边的配置改成你自己的小程序配置
27
+
28
+ ## 支付
29
+ 用户模型具有`buy`方法,而商品模型有`sell`方法,用户购买商品或商品售出都会产生一张订单,
30
+ 调用订单的`pay`方法则会向微信发起下单请求,并将处理后的结果返回,此时返回的是一个`ServiceResult`对象,
31
+ 执行该对象的`as_json`方法,得到的就是小程序需要拉起支付的参数
32
+
33
+ ```ruby
34
+ user = User.first
35
+
36
+ product = Product.first
37
+
38
+ # 微信要传一个用户调起支付的 ip: spbill_create_ip ,文档上写必传,但是传空、固定ip、不传都不会报错,
39
+ # 但是还是给他加上了,保存到用户模型的 last_spbill_create_ip 属性上,实际使用中可以
40
+ # 在控制器里边通过 request.remote_ip 获取请求的 ip
41
+ #
42
+ # user.spbill_create_ip = request.remote_ip
43
+ client_ip = "45.12.112.31"
44
+ user.spbill_create_ip = client_ip
45
+
46
+ order = user.buy(product)
47
+
48
+ result = order.pay
49
+
50
+ # 返回给前端调起支付的数据
51
+ result.as_json
52
+ ```
53
+
54
+ ## 退款
55
+ 调用订单的`refund`,参数是退款金额,默认全额退款,执行成功后会创建一张退款订单,此时退款订单状态为 pending,
56
+ 并返回退款下单的结果(`SerivceResult`对象),如果成功,触发用户商品关联模型的`refund_apply_success`,
57
+ 失败则触发`refund_apply_failure`
9
58
 
10
59
  ```ruby
11
- gem 'wechat_payment'
60
+ payment_order = User.first.payment_orders.last
61
+ result = payment_order.refund
62
+
63
+ if result.success?
64
+ # do something on success
65
+ else
66
+ # do something on failure
67
+ end
12
68
  ```
13
69
 
14
- And then execute:
15
- ```bash
16
- $ bundle
70
+ ## 事件
71
+ 当支付进行时,会产生一些相应的事件,事件会触发相应的钩子,如果有事先定义的话。钩子方法需定义在用户商品关联(如`UserProduct`)模型中,
72
+ 且每个钩子接收一个参数 result ,事件和钩子的对应关系如下:
73
+
74
+ | 事件 | 钩子 |
75
+ | :-------------- | :--------------- |
76
+ | 支付下单成功 | payment_apply_success |
77
+ | 支付下单失败 | payment_apply_failure |
78
+ | 支付成功(回调) | payment_exec_success |
79
+ | 支付失败(回调) | payment_exec_failure |
80
+ | 申请退款成功 | refund_apply_success |
81
+ | 申请退款失败 | refund_apply_failure |
82
+ | 退款成功(回调) | refund_exec_success |
83
+ | 退款失败(回调) | refund_exec_failure |
84
+
85
+ 一个简单的事件处理的例子
86
+ ```ruby
87
+ # app/model/user_product.rb
88
+ class UserProduct
89
+ # result 结构见下文说明
90
+ def payment_exec_success(result)
91
+ update(state: :paid)
92
+ end
93
+ end
17
94
  ```
18
95
 
19
- Or install it yourself as:
20
- ```bash
21
- $ gem install wechat_payment
96
+ 每个钩子接收的参数结构可能不一样:
97
+
98
+ ### payment_apply_success:
99
+ ```ruby
100
+ {
101
+ "return_code"=>"SUCCESS",
102
+ "return_msg"=>"OK",
103
+ "result_code"=>"SUCCESS",
104
+ "mch_id"=>"12312412312",
105
+ "appid"=>"wxc5acd06cc6a7ac12",
106
+ "sub_mch_id"=>"1526921451",
107
+ "sub_appid"=>"wxf89f912345823dcd",
108
+ "nonce_str"=>"ZUN2rEf6ATgYU8Lr",
109
+ "sign"=>"3A216DB61196CE2313CE21182D53FD1833F",
110
+ "prepay_id"=>"wx2815535651598812181c452eb3f26b90000",
111
+ "trade_type"=>"JSAPI"
112
+ }
22
113
  ```
23
114
 
115
+ ### payment_apply_failure:
116
+ ```ruby
117
+ {
118
+ "return_code"=>"SUCCESS",
119
+ "return_msg"=>"OK",
120
+ "result_code"=>"FAIL",
121
+ "err_code_des"=>"该订单已支付",
122
+ "err_code"=>"ORDERPAID",
123
+ "mch_id"=>"12312412312",
124
+ "appid"=>"wxc5acd06cc6a7ac12",
125
+ "sub_mch_id"=>"1526921451",
126
+ "sub_appid"=>"wxf89f912345823dcd",
127
+ "nonce_str"=>"1jWLkg2YZjwnOozl",
128
+ "sign"=>"3C210A1C9BD6CFDB7C37CCFCA1AAF9E274"
129
+ }
130
+ ```
131
+
132
+ ### payment_exec_success:
133
+ ```ruby
134
+ {
135
+ "appid" => "wxc5acd06cc6a7ac12",
136
+ "bank_type" => "CMB_CREDIT",
137
+ "cash_fee" => "1",
138
+ "fee_type" => "CNY",
139
+ "is_subscribe" => "N",
140
+ "mch_id" => "12312412312",
141
+ "nonce_str" => "e42ad44489a1f4e6f8a09d1299cfa59f6",
142
+ "openid" => "oef2nsaOcYcBYrNq1x9eUucKy7NQ",
143
+ "out_trade_no" => "16267654023120174189",
144
+ "result_code" => "SUCCESS",
145
+ "return_code" => "SUCCESS",
146
+ "sign" => "608972A854024030332360B1E79511B4",
147
+ "sub_appid" => "wxf89f912345823dcd",
148
+ "sub_is_subscribe" => "N",
149
+ "sub_mch_id" => "1526921451",
150
+ "sub_openid" => "oef2nsaOcYcBYrNq1x9eUucKy7NQ",
151
+ "time_end" => "20210720151228",
152
+ "total_fee" => "1",
153
+ "trade_type" => "JSAPI",
154
+ "transaction_id" => "4200001153233202137201270712453"
155
+ }
156
+ ```
157
+
158
+ ### payment_exec_failure:
159
+ TODO 待补充
160
+
161
+ ### refund_apply_success:
162
+ ```ruby
163
+ {
164
+ "return_code"=>"SUCCESS",
165
+ "return_msg"=>"OK",
166
+ "appid"=>"wxc5acd06cc6a7ac12",
167
+ "mch_id"=>"12312412312",
168
+ "sub_mch_id"=>"1526921451",
169
+ "nonce_str"=>"RsXVcs0GMg2p5NRD",
170
+ "sign"=>"F10AB3929B900DE4E189CA93B12D9D7A",
171
+ "result_code"=>"SUCCESS",
172
+ "transaction_id"=>"420000119112202101210049902399",
173
+ "out_trade_no"=>"1624867410141191608",
174
+ "out_refund_no"=>"16248674504421685776",
175
+ "refund_id"=>"50301108952025421210183695009",
176
+ "refund_channel"=>"",
177
+ "refund_fee"=>"1",
178
+ "coupon_refund_fee"=>"0",
179
+ "total_fee"=>"1",
180
+ "cash_fee"=>"1",
181
+ "coupon_refund_count"=>"0",
182
+ "cash_refund_fee"=>"1"
183
+ }
184
+ ```
185
+
186
+ ### refund_apply_failure:
187
+ ```ruby
188
+ {
189
+ "return_code"=>"SUCCESS",
190
+ "return_msg"=>"OK",
191
+ "appid"=>"wxc5acd06cc6a7ac12",
192
+ "mch_id"=>"12312412312",
193
+ "sub_mch_id"=>"1526921451",
194
+ "nonce_str"=>"gMDFilvaKanXW80W",
195
+ "sign"=>"BA24E81B18B12AAC112DF9F84CA5E21",
196
+ "result_code"=>"FAIL",
197
+ "err_code"=>"INVALID_REQUEST",
198
+ "err_code_des"=>"订单已全额退款"
199
+ }
200
+
201
+ ```
202
+
203
+ ### refund_exec_success:
204
+ ```ruby
205
+ {
206
+ "out_refund_no"=>"16421102919350974",
207
+ "out_trade_no"=>"1626761001219492162",
208
+ "refund_account"=>"REFUND_SOURCE_RECHARGE_FUNDS",
209
+ "refund_fee"=>"1",
210
+ "refund_id"=>"503014086722221012110826014924",
211
+ "refund_recv_accout"=>"招商银行信用卡4012",
212
+ "refund_request_source"=>"API",
213
+ "refund_status"=>"SUCCESS",
214
+ "settlement_refund_fee"=>"1",
215
+ "settlement_total_fee"=>"1",
216
+ "success_time"=>"2021-07-20 15:11:52",
217
+ "total_fee"=>"1",
218
+ "transaction_id"=>"42000011872021002028121998431"
219
+ }
220
+ ```
221
+
222
+ ### refund_exec_failure:
223
+ TODO 待补充
224
+
225
+
24
226
  ## Contributing
25
227
  Contribution directions go here.
26
228
 
27
229
  ## License
28
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
230
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -1,5 +1,10 @@
1
1
  require "bundler/setup"
2
2
 
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
3
8
  require "bundler/gem_tasks"
4
9
 
5
10
  require "rake/testtask"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/wechat_payment .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,5 @@
1
+ module WechatPayment
2
+ class ApplicationController < ActionController::Base
3
+ skip_forgery_protection
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+
2
+ module WechatPayment
3
+ class CallbackController < WechatPayment::ApplicationController
4
+
5
+ # 微信支付回调
6
+ def payment
7
+ result = Hash.from_xml(request.body.read)["xml"]
8
+
9
+ process_result = WechatPayment::Service.handle_payment_notify(result)
10
+ if process_result.success?
11
+ render xml: { return_code: "SUCCESS" }.to_xml(root: 'xml', dasherize: false)
12
+ else
13
+ render xml: { return_code: "FAIL", return_msg: process_result.errors.first }.to_xml(root: 'xml', dasherize: false)
14
+ end
15
+ end
16
+
17
+ def refund
18
+ callback_info = Hash.from_xml(request.body.read)["xml"]["req_info"]
19
+ refund_result = WechatPayment::Service.handle_refund_notify(callback_info)
20
+
21
+ if refund_result.success?
22
+ render xml: {return_code: "SUCCESS"}.to_xml(root: 'xml', dasherize: false)
23
+ else
24
+ render xml: {return_code: "FAIL", return_msg: refund_result.errors.first}.to_xml(root: 'xml', dasherize: false)
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ module WechatPayment
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module WechatPayment
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module WechatPayment
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module WechatPayment
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,228 @@
1
+ module WechatPayment
2
+ class PaymentOrder < ApplicationRecord
3
+
4
+ has_many :refund_orders
5
+ belongs_to :customer, polymorphic: true
6
+
7
+ before_save :set_customer_info
8
+ before_create :gen_out_trade_no
9
+ belongs_to :goods, polymorphic: true
10
+
11
+ enum state: {
12
+ paid: "paid",
13
+ pending: "pending",
14
+ refunded: "refunded",
15
+ failed: "failed"
16
+ }, _default: "pending"
17
+
18
+ # 将部分用户信息保存至订单
19
+ def set_customer_info
20
+ self.open_id = customer.open_id if open_id.blank?
21
+ self.spbill_create_ip = customer.spbill_create_ip
22
+ end
23
+
24
+ # 生成交易编号
25
+ def gen_out_trade_no
26
+ loop do
27
+ out_trade_no = "#{Time.current.to_i}#{SecureRandom.random_number(999_999_999)}"
28
+ records_count = WechatPayment::PaymentOrder.where(out_trade_no: out_trade_no).count
29
+ if records_count == 0
30
+ self.out_trade_no = out_trade_no
31
+ break
32
+ end
33
+ end
34
+ end
35
+
36
+ # 创建退款订单
37
+ def create_refund_order(refund_fee)
38
+ refund_orders.create(
39
+ out_trade_no: out_trade_no,
40
+ refund_fee: refund_fee,
41
+ total_fee: total_fee,
42
+ goods: goods,
43
+ customer: customer
44
+ )
45
+ end
46
+
47
+ def as_order_params
48
+ {
49
+ out_trade_no: out_trade_no,
50
+ spbill_create_ip: spbill_create_ip,
51
+ total_fee: total_fee,
52
+ body: body,
53
+ openid: open_id
54
+ }
55
+ end
56
+
57
+ # 发起支付
58
+ #
59
+ # @return [Hash]
60
+ #
61
+ # return example
62
+ # {
63
+ # "appId": "wxf89f9547da823dcd",
64
+ # "package": "prepay_id=wx28180521320799e04f6028c55c31bf0000",
65
+ # "nonceStr": "62350ff6c414946d0dc4c49b32ad3fd3",
66
+ # "timeStamp": "1624874721",
67
+ # "signType": "MD5",
68
+ # "paySign": "1F5CBC345B86E5DD055F235A22961422",
69
+ # "orderId": 17
70
+ # }
71
+ def pay
72
+ order_result = WechatPayment::Service.new(self).order
73
+ if order_result.success?
74
+ payload = WechatPayment::Client.gen_js_pay_payload(order_result.data).merge(orderId: id).with_indifferent_access
75
+ WechatPayment::ServiceResult.new(success: true, data: payload)
76
+ else
77
+ order_result
78
+ end
79
+ end
80
+
81
+ # 重新支付订单
82
+ def repay
83
+ gen_out_trade_no
84
+ save
85
+ pay
86
+ end
87
+
88
+ # 发起退款
89
+ # @param [Integer] refund_fee 需要退款的金额,单位:分
90
+ def refund(refund_fee = total_fee)
91
+ WechatPayment::Service.new(self).refund(refund_fee)
92
+ end
93
+
94
+ # 判断余额是否足够退款
95
+ def balance_enough_to_refund?(refund_fee)
96
+ total_fee - refunded_fee >= refund_fee
97
+ end
98
+
99
+ # 已退款的金额(包括正在退款的金额)
100
+ def refunded_fee
101
+ refund_orders.where(state: [:pending, :refunded]).sum(:refund_fee)
102
+ end
103
+
104
+ # 实际已退的金额
105
+ def actual_refunded_fee
106
+ refund_orders.where(state: :refunded).sum(:refund_fee)
107
+ end
108
+
109
+ # 订单是否可以退款
110
+ def refundable?
111
+ min_refund_fee = 1
112
+ paid? && balance_enough_to_refund?(min_refund_fee)
113
+ end
114
+
115
+ # 支付下单成功
116
+ # @param [Hash] result
117
+ #
118
+ # result example:
119
+ #
120
+ # {
121
+ # "return_code"=>"SUCCESS",
122
+ # "return_msg"=>"OK",
123
+ # "result_code"=>"SUCCESS",
124
+ # "mch_id"=>"12312412312",
125
+ # "appid"=>"wxc5f26065c6471234",
126
+ # "sub_mch_id"=>"1525911234",
127
+ # "sub_appid"=>"wxf89f912345823dcd",
128
+ # "nonce_str"=>"ZUN2rEf6ATgYU8Lr",
129
+ # "sign"=>"3A216DB61196CEC63CE282D53FD1833F",
130
+ # "prepay_id"=>"wx281553565159884f81c452eb3f26b90000",
131
+ # "trade_type"=>"JSAPI"
132
+ # }
133
+ def payment_apply_success(result)
134
+ update(prepay_id: result["prepay_id"])
135
+
136
+ if goods.respond_to? :payment_apply_success
137
+ goods.payment_apply_success(result)
138
+ end
139
+
140
+ result
141
+ end
142
+
143
+ # 支付下单失败
144
+ # @param [Hash] result
145
+ #
146
+ # result example:
147
+ #
148
+ # {
149
+ # "return_code"=>"SUCCESS",
150
+ # "return_msg"=>"OK",
151
+ # "result_code"=>"FAIL",
152
+ # "err_code_des"=>"该订单已支付",
153
+ # "err_code"=>"ORDERPAID",
154
+ # "mch_id"=>"1363241802",
155
+ # "appid"=>"wxc5f26065c6471bcf",
156
+ # "sub_mch_id"=>"1525918291",
157
+ # "sub_appid"=>"wxf89f9547da823dcd",
158
+ # "nonce_str"=>"1jWLkg2YZjwnOozl",
159
+ # "sign"=>"3C80A1C9BD6CFDB7C37CCFCEAAF9E274"
160
+ # }
161
+ def payment_apply_failure(result)
162
+ update(state: :failed)
163
+
164
+ if goods.respond_to? :payment_apply_failure
165
+ goods.payment_apply_failure(result)
166
+ end
167
+
168
+ result
169
+ end
170
+
171
+ # 支付成功(回调结果)
172
+ # @param [Hash] result
173
+ #
174
+ # result example:
175
+ #
176
+ # {
177
+ # "appid"=>"wxc5e21215c6471bcf",
178
+ # "bank_type"=>"CMB_CREDIT",
179
+ # "cash_fee"=>"1",
180
+ # "fee_type"=>"CNY",
181
+ # "is_subscribe"=>"N",
182
+ # "mch_id"=>"144223114",
183
+ # "nonce_str"=>"026b7ff3433f482f98610a67bbd8e159",
184
+ # "openid"=>"omf2nv3OgYXBYrNqdx9eUucKy7NQ",
185
+ # "out_trade_no"=>"1624866836218932068",
186
+ # "result_code"=>"SUCCESS",
187
+ # "return_code"=>"SUCCESS",
188
+ # "sign"=>"442098E6B670B82B88A843CC2A2AB54D",
189
+ # "sub_appid"=>"wxf89f95121da823dcd",
190
+ # "sub_is_subscribe"=>"N",
191
+ # "sub_mch_id"=>"1525911211",
192
+ # "sub_openid"=>"ogT7J5YddGnll-ippRvJq62Nv8W0",
193
+ # "time_end"=>"20210628155426",
194
+ # "total_fee"=>"1",
195
+ # "trade_type"=>"JSAPI",
196
+ # "transaction_id"=>"4200001174202106282207291730"
197
+ # }
198
+ def payment_exec_success(result)
199
+ update(
200
+ state: :paid,
201
+ transaction_id: result["transaction_id"],
202
+ paid_at: Time.current
203
+ )
204
+
205
+ if goods.respond_to? :payment_exec_success
206
+ goods.payment_exec_success(result)
207
+ end
208
+
209
+ result
210
+ end
211
+
212
+ # 支付失败(回调结果)
213
+ # @param [Hash] result
214
+ #
215
+ # result example:
216
+ #
217
+ def payment_exec_failure(result)
218
+ if goods.respond_to? :payment_exec_failure
219
+ goods.payment_exec_failure(result)
220
+ end
221
+ end
222
+
223
+ # 判断是否已经全额退款
224
+ def total_fee_refunded?
225
+ refunded_fee >= total_fee
226
+ end
227
+ end
228
+ end