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,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