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,127 @@
1
+ module WechatPayment
2
+ class RefundOrder < ApplicationRecord
3
+ belongs_to :payment_order
4
+
5
+ before_create :gen_out_refund_no
6
+
7
+ belongs_to :goods, polymorphic: true
8
+ belongs_to :customer, polymorphic: true
9
+
10
+ enum state: {
11
+ pending: "pending",
12
+ refunded: "refunded",
13
+ failed: "failed"
14
+ }, _default: :pending
15
+
16
+ # 生成退款编号
17
+ def gen_out_refund_no
18
+ loop do
19
+ out_refund_no = "#{Time.current.to_i}#{SecureRandom.random_number(999_999_999)}"
20
+ record = WechatPayment::RefundOrder.find_by(out_refund_no: out_refund_no)
21
+
22
+ if record.blank?
23
+ self.out_refund_no = out_refund_no
24
+ break
25
+ end
26
+ end
27
+ end
28
+
29
+ def as_refund_params
30
+ slice(:out_trade_no, :out_refund_no, :refund_fee, :total_fee).to_options
31
+ end
32
+ # 发起退款成功
33
+ # @param [Hash] result
34
+ #
35
+ # result example:
36
+ #
37
+ # {
38
+ # "return_code"=>"SUCCESS",
39
+ # "return_msg"=>"OK",
40
+ # "appid"=>"wxc5f2606121234cf",
41
+ # "mch_id"=>"1363241234",
42
+ # "sub_mch_id"=>"1525912341",
43
+ # "nonce_str"=>"RsXVcs0GMg2p5NRD",
44
+ # "sign"=>"F10AB3929B900DE4E189CA93B73D9D7A",
45
+ # "result_code"=>"SUCCESS",
46
+ # "transaction_id"=>"4200001199202106280049902399",
47
+ # "out_trade_no"=>"1624867410475591608",
48
+ # "out_refund_no"=>"1624867450917685776",
49
+ # "refund_id"=>"50301108952021062810183695009",
50
+ # "refund_channel"=>"",
51
+ # "refund_fee"=>"1",
52
+ # "coupon_refund_fee"=>"0",
53
+ # "total_fee"=>"1",
54
+ # "cash_fee"=>"1",
55
+ # "coupon_refund_count"=>"0",
56
+ # "cash_refund_fee"=>"1"
57
+ # }
58
+ def refund_apply_success(result)
59
+ if payment_order.goods.respond_to? :refund_apply_success
60
+ payment_order.goods.refund_apply_success(result)
61
+ end
62
+
63
+ update(
64
+ refund_id: result["refund_id"],
65
+ state: :pending
66
+ )
67
+
68
+ result
69
+ end
70
+
71
+ # 发起退款失败
72
+ def refund_apply_failure(result)
73
+ # TODO 没遇到过,待补充
74
+
75
+ if payment_order.goods.respond_to? :refund_apply_failure
76
+ payment_order.goods.refund_apply_failure(result)
77
+ end
78
+ end
79
+
80
+ # 退款成功(回调)
81
+ # @param [Hash] result
82
+ #
83
+ # result example:
84
+ #
85
+ # {
86
+ # "out_refund_no"=>"1624873658515277479",
87
+ # "out_trade_no"=>"1624873575281298144",
88
+ # "refund_account"=>"REFUND_SOURCE_RECHARGE_FUNDS",
89
+ # "refund_fee"=>"1",
90
+ # "refund_id"=>"50301308842021062810182580986",
91
+ # "refund_recv_accout"=>"招商银行信用卡4003",
92
+ # "refund_request_source"=>"API",
93
+ # "refund_status"=>"SUCCESS",
94
+ # "settlement_refund_fee"=>"1",
95
+ # "settlement_total_fee"=>"1",
96
+ # "success_time"=>"2021-06-28 17:47:47",
97
+ # "total_fee"=>"1",
98
+ # "transaction_id"=>"4200001202202106280268010129"
99
+ # }
100
+ def refund_exec_success(result)
101
+ update(
102
+ state: :refunded,
103
+ refunded_at: Time.current
104
+ )
105
+
106
+ if payment_order.total_fee_refunded?
107
+ payment_order.update(state: :refunded, refunded_at: Time.current)
108
+ end
109
+
110
+ if payment_order.goods.respond_to? :refund_exec_success
111
+ payment_order.goods.refund_exec_success(result)
112
+ end
113
+
114
+ result
115
+ end
116
+
117
+ # 退款失败(回调)
118
+ def refund_exec_failure(result)
119
+ # TODO 待补充
120
+ if payment_order.goods.respond_to? :refund_exec_failure
121
+ payment_order.goods.refund_exec_failure(result)
122
+ end
123
+ end
124
+
125
+
126
+ end
127
+ end
@@ -0,0 +1,74 @@
1
+
2
+ module WechatPayment
3
+ class Service
4
+ attr_reader :client, :payment_order
5
+
6
+ def initialize(payment_order)
7
+ @client = WechatPayment::Client.new
8
+ @payment_order = payment_order
9
+ end
10
+
11
+ # 下单
12
+ def order
13
+ order_result = client.order(payment_order.as_order_params)
14
+
15
+ if order_result.success?
16
+ payment_order.payment_apply_success(order_result.data)
17
+ else
18
+ payment_order.payment_apply_failure(order_result.errors)
19
+ end
20
+
21
+ order_result
22
+ end
23
+
24
+ # 退款
25
+ def refund(refund_fee)
26
+ if !payment_order.balance_enough_to_refund?(refund_fee)
27
+ return WechatPayment::ServiceResult.new(message_type: :error, message: "Balance is not enough.")
28
+ end
29
+
30
+ refund_order = payment_order.create_refund_order(refund_fee)
31
+ refund_result = client.refund(refund_order.as_refund_params)
32
+
33
+ if refund_result.success?
34
+ refund_order.refund_apply_success(refund_result.data)
35
+ else
36
+ refund_order.refund_apply_failure(refund_result.errors)
37
+ end
38
+
39
+ refund_result
40
+ end
41
+
42
+ # 处理支付回调
43
+ def self.handle_payment_notify(notify_data)
44
+ result = WechatPayment::Client.handle_payment_notify(notify_data)
45
+ payment_order = WechatPayment::PaymentOrder.find_by(out_trade_no: notify_data["out_trade_no"])
46
+
47
+ if result.success? && payment_order.pending?
48
+ payment_order.with_lock do
49
+ payment_order.payment_exec_success(result.data)
50
+ end
51
+ else
52
+ payment_order.payment_exec_failure(result.errors)
53
+ end
54
+
55
+ result
56
+ end
57
+
58
+ # 处理退款回调
59
+ def self.handle_refund_notify(notify_data)
60
+ result = WechatPayment::Client.handle_refund_notify(notify_data)
61
+ refund_order = WechatPayment::RefundOrder.find_by(out_refund_no: result.data["out_refund_no"])
62
+
63
+ if result.success? && refund_order.pending?
64
+ refund_order.with_lock do
65
+ refund_order.refund_exec_success(result.data)
66
+ end
67
+ else
68
+ refund_order.refund_exec_failure(result.errors)
69
+ end
70
+
71
+ result
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Wechat payment</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "wechat_payment/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ WechatPayment::Engine.routes.draw do
2
+ post "/callback/payment", to: "callback#payment"
3
+ post "/callback/refund", to: "callback#refund"
4
+ end
@@ -0,0 +1,22 @@
1
+ class CreateWechatPaymentPaymentOrders < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table :wechat_payment_payment_orders do |t|
4
+ t.string :open_id
5
+ t.string :out_trade_no
6
+ t.references :goods, polymorphic: true, null: false
7
+ t.references :customer, polymorphic: true, null: false
8
+ t.string :transaction_id
9
+ t.string :body
10
+ t.integer :total_fee
11
+ t.string :trade_type
12
+ t.string :spbill_create_ip
13
+ t.string :prepay_id
14
+ t.string :state
15
+ t.datetime :paid_at
16
+ t.datetime :refunded_at
17
+
18
+ t.timestamps
19
+ end
20
+ add_index :wechat_payment_payment_orders, :open_id
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ class CreateWechatPaymentRefundOrders < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table :wechat_payment_refund_orders do |t|
4
+ t.integer :payment_order_id
5
+ t.references :goods, polymorphic: true, null: false
6
+ t.references :customer, polymorphic: true, null: false
7
+ t.integer :refund_fee
8
+ t.integer :total_fee
9
+ t.string :out_trade_no
10
+ t.string :out_refund_no
11
+ t.string :refund_id
12
+ t.string :state
13
+ t.datetime :refunded_at
14
+
15
+ t.timestamps
16
+
17
+ t.index :payment_order_id, name: "payment_id_on_refund_orders"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ bin/rails generate goods Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,5 @@
1
+ class WechatPayment::GoodsGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('templates', __dir__)
3
+
4
+
5
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ bin/rails generate install Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,86 @@
1
+ class WechatPayment::InstallGenerator < Rails::Generators::Base
2
+ source_root File.expand_path('templates', __dir__)
3
+
4
+ argument :goods, type: :string
5
+ argument :user, type: :string, default: :User
6
+
7
+
8
+ # 生成 initializer 文件
9
+ def gen_initializer_file
10
+ copy_file "initializer.rb", "config/initializers/wechat_payment.rb"
11
+ end
12
+
13
+ # 挂载 engine 到路由上
14
+ def mount_payment_engine
15
+ route %Q(mount WechatPayment::Engine => "/wechat_payment")
16
+ end
17
+
18
+ # 安装迁移文件
19
+ def copy_migration
20
+ rake "wechat_payment:install:migrations"
21
+ end
22
+
23
+ def add_concern_to_goods
24
+ goods_model_head_one = "class #{goods_model_name} < ApplicationRecord"
25
+ inject_into_file goods_model_file, after: goods_model_head_one do <<-GOODS_CONCERN
26
+
27
+ include WechatPayment::Concern::Goods
28
+ #{def_custom_user_model}
29
+ GOODS_CONCERN
30
+ end
31
+ end
32
+
33
+ def add_concern_to_users
34
+ user_model_head_one = "class #{user_model_name} < ApplicationRecord"
35
+ inject_into_file user_model_file, after: user_model_head_one do <<-'USERS_CONCERN'
36
+
37
+ include WechatPayment::Concern::User
38
+ USERS_CONCERN
39
+ end
40
+ end
41
+
42
+ def add_concern_to_user_goods
43
+ user_goods_model_head_one = "class #{user_goods_model_name} < ApplicationRecord"
44
+ inject_into_file user_goods_model_file, after: user_goods_model_head_one do <<-'USER_GOOD_CONCERN'
45
+
46
+ include WechatPayment::Concern::UserGoods
47
+ USER_GOOD_CONCERN
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def goods_model_file
54
+ "app/models/#{goods.to_s.underscore}.rb"
55
+ end
56
+
57
+ def user_model_file
58
+ "app/models/#{user.to_s.underscore}.rb"
59
+ end
60
+
61
+ def user_goods_model_file
62
+ "app/models/#{user.to_s.underscore}_#{goods.to_s.underscore}.rb"
63
+ end
64
+
65
+ def goods_model_name
66
+ goods.to_s.camelize
67
+ end
68
+
69
+ def user_model_name
70
+ user.to_s.camelize
71
+ end
72
+
73
+ def user_goods_model_name
74
+ user_model_name + goods_model_name
75
+ end
76
+
77
+ def def_custom_user_model
78
+ if user_model_name != 'User'
79
+ <<-DEF
80
+ self.user_model = "#{user_model_name}"
81
+ self.user_ref_field = "#{user_model_name.underscore}"
82
+ self.user_goods_model = "#{user_model_name}#{goods_model_name}"
83
+ DEF
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,19 @@
1
+
2
+ WechatPayment.setup do |config|
3
+ # 接收回调的域名
4
+ config.host = "https://xxx.com"
5
+
6
+ # 下面所有参数都需要改成你自己的小程序配置
7
+ config.appid = "wxc5c26065c6123456"
8
+ config.app_secret = "123456784aa91bb538867d3d2790b308"
9
+ config.mch_id = "112241802"
10
+ config.key = "123456723erivPO09irNNbh78u8udwFer"
11
+
12
+ # 证书可以在微信支付后台获取到,路径是相对于项目根路径,如果需要退款的话,则必须要证书
13
+ # config.cert_path = "config/apiclient_cert.p12"
14
+ config.cert_path = nil
15
+
16
+ config.sub_appid = "wx8f9f912623456789"
17
+ config.sub_mch_id = "1234911291"
18
+ config.sub_app_secret = "88888231e2f3a21152d163f61b99999"
19
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ bin/rails generate routes Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,7 @@
1
+ class WechatPayment::RoutesGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('templates', __dir__)
3
+
4
+ def mount_wechat_payment
5
+ route %Q(mount WechatPayment::Engine => "/wechat_payment")
6
+ end
7
+ end
@@ -2,3 +2,13 @@
2
2
  # task :wechat_payment do
3
3
  # # Task goes here
4
4
  # end
5
+
6
+ desc "Install Wechat Payment Engine"
7
+ namespace :wechat_payment do
8
+ task :install, [:goods, :user] => :environment do |task, args|
9
+ Rake::Task["wechat_payment:install:migrations"].invoke
10
+ sh "rails g wechat_payment:initializer wechat_payment"
11
+ sh "rails g wechat_payment:routes wechat_payment"
12
+ sh "rails g wechat_payment:goods #{args.goods} user:#{args.user.presence || 'user'}"
13
+ end
14
+ end
@@ -1,6 +1,44 @@
1
1
  require "wechat_payment/version"
2
- require "wechat_payment/railtie"
2
+ require "wechat_payment/engine"
3
3
 
4
4
  module WechatPayment
5
- # Your code goes here...
5
+
6
+ class << self
7
+ attr_reader :apiclient_cert, :apiclient_key
8
+ attr_accessor :appid, :app_secret, :mch_id, :sub_appid, :sub_app_secret, :sub_mch_id, :key, :cert_path, :host
9
+ end
10
+
11
+ def self.setup
12
+ yield self if block_given?
13
+
14
+ if cert_path
15
+ set_apiclient_by_pkcs12(File.binread(cert_path), mch_id)
16
+ end
17
+ end
18
+
19
+ def self.set_apiclient_by_pkcs12(str, pass)
20
+ pkcs12 = OpenSSL::PKCS12.new(str, pass)
21
+ @apiclient_cert = pkcs12.certificate
22
+ @apiclient_key = pkcs12.key
23
+
24
+ pkcs12
25
+ end
26
+
27
+ def apiclient_cert=(cert)
28
+ @apiclient_cert = OpenSSL::X509::Certificate.new(cert)
29
+ end
30
+
31
+ def apiclient_key=(key)
32
+ @apiclient_key = OpenSSL::PKey::RSA.new(key)
33
+ end
34
+
35
+ def self.as_payment_params
36
+ {
37
+ appid: appid,
38
+ mch_id: mch_id,
39
+ sub_appid: sub_appid,
40
+ sub_mch_id: sub_mch_id
41
+ }
42
+ end
43
+
6
44
  end