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.
- checksums.yaml +4 -4
- data/README.md +215 -13
- data/Rakefile +5 -0
- data/app/assets/config/wechat_payment_manifest.js +1 -0
- data/app/assets/stylesheets/wechat_payment/application.css +15 -0
- data/app/controllers/wechat_payment/application_controller.rb +5 -0
- data/app/controllers/wechat_payment/callback_controller.rb +30 -0
- data/app/helpers/wechat_payment/application_helper.rb +4 -0
- data/app/jobs/wechat_payment/application_job.rb +4 -0
- data/app/mailers/wechat_payment/application_mailer.rb +6 -0
- data/app/models/wechat_payment/application_record.rb +5 -0
- data/app/models/wechat_payment/payment_order.rb +228 -0
- data/app/models/wechat_payment/refund_order.rb +127 -0
- data/app/services/wechat_payment/service.rb +74 -0
- data/app/views/layouts/wechat_payment/application.html.erb +15 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20210706075217_create_wechat_payment_payment_orders.rb +22 -0
- data/db/migrate/20210706095205_create_wechat_payment_refund_orders.rb +20 -0
- data/lib/generators/wechat_payment/goods/USAGE +8 -0
- data/lib/generators/wechat_payment/goods/goods_generator.rb +5 -0
- data/lib/generators/wechat_payment/install/USAGE +8 -0
- data/lib/generators/wechat_payment/install/install_generator.rb +86 -0
- data/lib/generators/wechat_payment/install/templates/initializer.rb +19 -0
- data/lib/generators/wechat_payment/routes/USAGE +8 -0
- data/lib/generators/wechat_payment/routes/routes_generator.rb +7 -0
- data/lib/tasks/wechat_payment_tasks.rake +10 -0
- data/lib/wechat_payment.rb +40 -2
- data/lib/wechat_payment/client.rb +272 -0
- data/lib/wechat_payment/concern/goods.rb +80 -0
- data/lib/wechat_payment/concern/user.rb +22 -0
- data/lib/wechat_payment/concern/user_goods.rb +14 -0
- data/lib/wechat_payment/engine.rb +7 -0
- data/lib/wechat_payment/invoke_result.rb +35 -0
- data/lib/wechat_payment/missing_key_error.rb +4 -0
- data/lib/wechat_payment/r_logger.rb +36 -0
- data/lib/wechat_payment/service_result.rb +49 -0
- data/lib/wechat_payment/sign.rb +41 -0
- data/lib/wechat_payment/version.rb +1 -1
- metadata +68 -9
- 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
|
data/config/routes.rb
ADDED
@@ -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,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
|
@@ -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
|
data/lib/wechat_payment.rb
CHANGED
@@ -1,6 +1,44 @@
|
|
1
1
|
require "wechat_payment/version"
|
2
|
-
require "wechat_payment/
|
2
|
+
require "wechat_payment/engine"
|
3
3
|
|
4
4
|
module WechatPayment
|
5
|
-
|
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
|