allinpay_cnp 0.1.1
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.
- checksums.yaml +7 -0
- data/README.md +219 -0
- data/lib/allinpay_cnp/client.rb +119 -0
- data/lib/allinpay_cnp/config.rb +30 -0
- data/lib/allinpay_cnp/request.rb +88 -0
- data/lib/allinpay_cnp/response.rb +137 -0
- data/lib/allinpay_cnp/signature.rb +70 -0
- data/lib/allinpay_cnp/version.rb +5 -0
- data/lib/allinpay_cnp.rb +29 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d6a122d0fc477863ecbad367004a05ecae0b0f1876fe04ba82d3c1e24a6bad69
|
|
4
|
+
data.tar.gz: 88c28a107d766171b93f35bd0e78ff33324503f85401191e904899dd72a45d0b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6cf074a1b6b0de55d3beb38c9624fc3a784bd5896af05b6f82838fc24b4e0914f769815e5bf6e5345c1752cedc5688a4c1490d423ee6c47b2452ad08dcd33c79
|
|
7
|
+
data.tar.gz: adfa675fc31a869b101527c8e54aa3b021ff468f8c33254051fb197a22581f3da027cf6af346353041610e6bc7c2669f6c2532e008df3e71d0c6c4b088021870
|
data/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# AllinpayCnp
|
|
2
|
+
|
|
3
|
+
通联支付 CNP 跨境信用卡收单 Ruby SDK。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
### Gemfile
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'allinpay_cnp', git: 'https://github.com/your-org/allinpay_cnp'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 本地安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd allinpay_cnp
|
|
17
|
+
gem build allinpay_cnp.gemspec
|
|
18
|
+
gem install allinpay_cnp-0.1.0.gem
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 配置
|
|
22
|
+
|
|
23
|
+
### Rails 项目
|
|
24
|
+
|
|
25
|
+
创建 `config/initializers/allinpay_cnp.rb`:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
AllinpayCnp.configure do |config|
|
|
29
|
+
config.merchant_id = Rails.application.credentials.dig(:allinpay, :merchant_id)
|
|
30
|
+
config.private_key = Rails.application.credentials.dig(:allinpay, :private_key)
|
|
31
|
+
config.public_key = Rails.application.credentials.dig(:allinpay, :public_key)
|
|
32
|
+
config.environment = Rails.env.production? ? :production : :test
|
|
33
|
+
config.timeout = 30
|
|
34
|
+
config.logger = Rails.logger
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 普通 Ruby 项目
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require 'allinpay_cnp'
|
|
42
|
+
|
|
43
|
+
AllinpayCnp.configure do |config|
|
|
44
|
+
config.merchant_id = '086310030670001'
|
|
45
|
+
config.private_key = File.read('private_key.pem')
|
|
46
|
+
config.public_key = File.read('public_key.pem')
|
|
47
|
+
config.environment = :test # :test 或 :production
|
|
48
|
+
config.timeout = 30
|
|
49
|
+
config.logger = Logger.new($stdout)
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 配置项
|
|
54
|
+
|
|
55
|
+
| 配置项 | 类型 | 必填 | 说明 |
|
|
56
|
+
|--------|------|------|------|
|
|
57
|
+
| `merchant_id` | String | 是 | 商户号 |
|
|
58
|
+
| `private_key` | String | 是 | 商户私钥 (PEM 格式) |
|
|
59
|
+
| `public_key` | String | 否 | CNP 公钥,用于验证回调签名 |
|
|
60
|
+
| `environment` | Symbol | 否 | `:test` (默认) 或 `:production` |
|
|
61
|
+
| `timeout` | Integer | 否 | 请求超时时间,默认 30 秒 |
|
|
62
|
+
| `logger` | Logger | 否 | 日志对象 |
|
|
63
|
+
|
|
64
|
+
## 使用方法
|
|
65
|
+
|
|
66
|
+
### 获取客户端
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
client = AllinpayCnp.client
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 统一收银台支付 (Unified Pay)
|
|
73
|
+
|
|
74
|
+
跳转到通联支付页面完成支付:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
response = client.unified_pay(
|
|
78
|
+
access_order_id: "ORDER_#{Time.now.to_i}",
|
|
79
|
+
amount: '100.00',
|
|
80
|
+
currency: 'HKD',
|
|
81
|
+
urls: {
|
|
82
|
+
notify_url: 'https://your-domain.com/webhooks/allinpay',
|
|
83
|
+
return_url: 'https://your-domain.com/payments/complete'
|
|
84
|
+
},
|
|
85
|
+
email: 'customer@example.com',
|
|
86
|
+
language: 'zh-hant', # 可选: zh-hant, zh-hans, en
|
|
87
|
+
shipping: {
|
|
88
|
+
first_name: 'Peter',
|
|
89
|
+
last_name: 'Zhang',
|
|
90
|
+
address1: '123 Test Street',
|
|
91
|
+
city: 'Hong Kong',
|
|
92
|
+
country: 'HK',
|
|
93
|
+
zip_code: '000000',
|
|
94
|
+
phone: '12345678'
|
|
95
|
+
},
|
|
96
|
+
billing: {
|
|
97
|
+
first_name: 'Peter',
|
|
98
|
+
last_name: 'Zhang',
|
|
99
|
+
address1: '123 Test Street',
|
|
100
|
+
city: 'Hong Kong',
|
|
101
|
+
country: 'HK',
|
|
102
|
+
zip_code: '000000',
|
|
103
|
+
phone: '12345678'
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if response.success?
|
|
108
|
+
redirect_to response.payment_url
|
|
109
|
+
else
|
|
110
|
+
puts "Error: #{response.result_desc}"
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 查询订单 (Query)
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
response = client.query('ORIGINAL_ORDER_123')
|
|
118
|
+
|
|
119
|
+
if response.success?
|
|
120
|
+
puts "状态: #{response.status}" # SUCCESS, FAIL, PROCESSING
|
|
121
|
+
puts "金额: #{response.amount}"
|
|
122
|
+
puts "币种: #{response.currency}"
|
|
123
|
+
puts "卡号: #{response.card_no}" # 脱敏卡号
|
|
124
|
+
puts "卡组织: #{response.card_orgn}" # VISA, MASTERCARD 等
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 退款 (Refund)
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
response = client.refund(
|
|
132
|
+
ori_access_order_id: 'ORIGINAL_ORDER_123',
|
|
133
|
+
refund_amount: '50.00'
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if response.success?
|
|
137
|
+
puts '退款成功'
|
|
138
|
+
else
|
|
139
|
+
puts "退款失败: #{response.result_desc}"
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 验证回调签名
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
# Rails Controller
|
|
147
|
+
class WebhooksController < ApplicationController
|
|
148
|
+
skip_before_action :verify_authenticity_token, only: [:allinpay]
|
|
149
|
+
|
|
150
|
+
def allinpay
|
|
151
|
+
client = AllinpayCnp.client
|
|
152
|
+
|
|
153
|
+
unless client.verify_callback(params.to_unsafe_h)
|
|
154
|
+
render plain: 'FAIL' and return
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if params[:resultCode] == '0000'
|
|
158
|
+
payment = Payment.find_by(gateway_order_id: params[:accessOrderId])
|
|
159
|
+
payment&.mark_as_paid!(
|
|
160
|
+
transaction_ref: params[:orderId],
|
|
161
|
+
card_no: params[:cardNo],
|
|
162
|
+
card_orgn: params[:cardOrgn]
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
render plain: 'SUCCESS' # 必须返回 SUCCESS
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Response 对象
|
|
172
|
+
|
|
173
|
+
所有 API 方法都返回 `Response` 对象:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
response.success? # 是否成功 (resultCode == '0000')
|
|
177
|
+
response.failure? # 是否失败
|
|
178
|
+
response.result_code # 返回码
|
|
179
|
+
response.result_desc # 返回描述
|
|
180
|
+
response.payment_url # 支付页面 URL (unified_pay)
|
|
181
|
+
response.access_order_id # 商户订单号
|
|
182
|
+
response.order_id # CNP 系统订单号
|
|
183
|
+
response.status # 订单状态 (query)
|
|
184
|
+
response.amount # 金额
|
|
185
|
+
response.currency # 币种
|
|
186
|
+
response.card_no # 脱敏卡号
|
|
187
|
+
response.card_orgn # 卡组织
|
|
188
|
+
response.body # 原始响应 Hash
|
|
189
|
+
response['customField'] # 获取任意字段
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## API 地址
|
|
193
|
+
|
|
194
|
+
| 环境 | Unified Pay | QuickPay |
|
|
195
|
+
|------|-------------|----------|
|
|
196
|
+
| 测试 | https://cnp-test.allinpay.com/gateway/cnp/unifiedPay | https://cnp-test.allinpay.com/gateway/cnp/quickpay |
|
|
197
|
+
| 生产 | https://cnp.allinpay.com/gateway/cnp/unifiedPay | https://cnp.allinpay.com/gateway/cnp/quickpay |
|
|
198
|
+
|
|
199
|
+
## 运行测试
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
bundle install
|
|
203
|
+
bundle exec rspec
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## 目录结构
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
lib/
|
|
210
|
+
├── allinpay_cnp.rb # 主入口
|
|
211
|
+
└── allinpay_cnp/
|
|
212
|
+
├── version.rb # 版本号
|
|
213
|
+
├── config.rb # 配置类
|
|
214
|
+
├── signature.rb # RSA2 签名
|
|
215
|
+
├── request.rb # HTTP 请求
|
|
216
|
+
├── response.rb # 响应封装
|
|
217
|
+
└── client.rb # API 客户端
|
|
218
|
+
```
|
|
219
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllinpayCnp
|
|
4
|
+
class Client
|
|
5
|
+
VERSION = 'V2.0.0'
|
|
6
|
+
|
|
7
|
+
def unified_pay(access_order_id:, amount:, currency:, urls:, **options)
|
|
8
|
+
params = build_unified_pay_params(
|
|
9
|
+
access_order_id: access_order_id,
|
|
10
|
+
amount: amount,
|
|
11
|
+
currency: currency,
|
|
12
|
+
urls: urls,
|
|
13
|
+
**options
|
|
14
|
+
)
|
|
15
|
+
request.post(:unified_pay, params)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def query(ori_access_order_id)
|
|
19
|
+
params = {
|
|
20
|
+
version: VERSION,
|
|
21
|
+
mchtId: config.merchant_id,
|
|
22
|
+
transType: 'Query',
|
|
23
|
+
oriAccessOrderId: ori_access_order_id
|
|
24
|
+
}
|
|
25
|
+
request.post(:quickpay, params)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def refund(ori_access_order_id:, refund_amount:, access_order_id: nil)
|
|
29
|
+
params = {
|
|
30
|
+
version: VERSION,
|
|
31
|
+
mchtId: config.merchant_id,
|
|
32
|
+
transType: 'Refund',
|
|
33
|
+
accessOrderId: access_order_id,
|
|
34
|
+
oriAccessOrderId: ori_access_order_id,
|
|
35
|
+
refundAmount: refund_amount.to_s
|
|
36
|
+
}
|
|
37
|
+
request.post(:quickpay, params)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def verify_callback(params)
|
|
41
|
+
return false unless config.public_key
|
|
42
|
+
|
|
43
|
+
Signature.verify(params, config.public_key)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def config
|
|
49
|
+
AllinpayCnp.config
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def request
|
|
53
|
+
@request ||= Request.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_unified_pay_params(access_order_id:, amount:, currency:, urls:, **options)
|
|
57
|
+
build_base_params(access_order_id, amount, currency, urls, options)
|
|
58
|
+
.merge(build_shipping_params(options))
|
|
59
|
+
.merge(build_billing_params(options))
|
|
60
|
+
.compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_base_params(access_order_id, amount, currency, urls, options)
|
|
64
|
+
build_order_core_params(access_order_id, amount, currency)
|
|
65
|
+
.merge(notify_return_urls(urls))
|
|
66
|
+
.merge(build_base_option_params(options))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_order_core_params(access_order_id, amount, currency)
|
|
70
|
+
{
|
|
71
|
+
version: VERSION,
|
|
72
|
+
mchtId: config.merchant_id,
|
|
73
|
+
accessOrderId: access_order_id,
|
|
74
|
+
amount: amount.to_s,
|
|
75
|
+
currency: currency
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def notify_return_urls(urls)
|
|
80
|
+
{ notifyUrl: urls[:notify_url], returnUrl: urls[:return_url] }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_base_option_params(options)
|
|
84
|
+
{
|
|
85
|
+
language: options[:language] || 'zh-hant',
|
|
86
|
+
email: options[:email],
|
|
87
|
+
productInfo: options[:product_info]&.to_json
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_shipping_params(options)
|
|
92
|
+
{
|
|
93
|
+
shippingFirstName: options.dig(:shipping, :first_name),
|
|
94
|
+
shippingLastName: options.dig(:shipping, :last_name),
|
|
95
|
+
shippingAddress1: options.dig(:shipping, :address1),
|
|
96
|
+
shippingAddress2: options.dig(:shipping, :address2),
|
|
97
|
+
shippingCity: options.dig(:shipping, :city),
|
|
98
|
+
shippingState: options.dig(:shipping, :state),
|
|
99
|
+
shippingCountry: options.dig(:shipping, :country),
|
|
100
|
+
shippingZipCode: options.dig(:shipping, :zip_code),
|
|
101
|
+
shippingPhone: options.dig(:shipping, :phone)
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_billing_params(options)
|
|
106
|
+
{
|
|
107
|
+
billingFirstName: options.dig(:billing, :first_name),
|
|
108
|
+
billingLastName: options.dig(:billing, :last_name),
|
|
109
|
+
billingAddress1: options.dig(:billing, :address1),
|
|
110
|
+
billingAddress2: options.dig(:billing, :address2),
|
|
111
|
+
billingCity: options.dig(:billing, :city),
|
|
112
|
+
billingState: options.dig(:billing, :state),
|
|
113
|
+
billingCountry: options.dig(:billing, :country),
|
|
114
|
+
billingZipCode: options.dig(:billing, :zip_code),
|
|
115
|
+
billingPhone: options.dig(:billing, :phone)
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllinpayCnp
|
|
4
|
+
class Config
|
|
5
|
+
ENVIRONMENTS = %i[test production].freeze
|
|
6
|
+
|
|
7
|
+
attr_accessor :merchant_id, :private_key, :public_key, :logger, :timeout
|
|
8
|
+
attr_reader :environment
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@environment = :test
|
|
12
|
+
@timeout = 30
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def environment=(value)
|
|
16
|
+
value = value.to_sym
|
|
17
|
+
raise ArgumentError, "Invalid environment: #{value}" unless ENVIRONMENTS.include?(value)
|
|
18
|
+
|
|
19
|
+
@environment = value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test?
|
|
23
|
+
@environment == :test
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def production?
|
|
27
|
+
@environment == :production
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module AllinpayCnp
|
|
7
|
+
class Request
|
|
8
|
+
ENDPOINTS = {
|
|
9
|
+
test: {
|
|
10
|
+
quickpay: 'https://cnp-test.allinpay.com/gateway/cnp/quickpay',
|
|
11
|
+
unified_pay: 'https://cnp-test.allinpay.com/gateway/cnp/unifiedPay'
|
|
12
|
+
},
|
|
13
|
+
production: {
|
|
14
|
+
quickpay: 'https://cnp.allinpay.com/gateway/cnp/quickpay',
|
|
15
|
+
unified_pay: 'https://cnp.allinpay.com/gateway/cnp/unifiedPay'
|
|
16
|
+
}
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def post(endpoint_type, params)
|
|
20
|
+
sign_params(params)
|
|
21
|
+
url = build_url(endpoint_type)
|
|
22
|
+
log_request(url, params)
|
|
23
|
+
response = send_post(url, params)
|
|
24
|
+
log_response(response)
|
|
25
|
+
Response.new(response, public_key: config.public_key)
|
|
26
|
+
rescue Faraday::Error => e
|
|
27
|
+
log_error(e.message)
|
|
28
|
+
Response.new(nil, error: e)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def config
|
|
34
|
+
AllinpayCnp.config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def sign_params(params)
|
|
38
|
+
params[:signType] = 'RSA2'
|
|
39
|
+
params[:sign] = Signature.sign(params, config.private_key)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_url(endpoint_type)
|
|
43
|
+
ENDPOINTS[config.environment][endpoint_type]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def send_post(url, params)
|
|
47
|
+
connection.post(url) do |req|
|
|
48
|
+
req.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
49
|
+
req.body = encode_params(params)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def encode_params(params)
|
|
54
|
+
params
|
|
55
|
+
.reject { |_, v| v.nil? || v.to_s.empty? }
|
|
56
|
+
.map { |k, v| "#{k}=#{URI.encode_www_form_component(v.to_s)}" }
|
|
57
|
+
.join('&')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def connection
|
|
61
|
+
@connection ||= Faraday.new do |conn|
|
|
62
|
+
conn.options.timeout = config.timeout
|
|
63
|
+
conn.options.open_timeout = config.timeout
|
|
64
|
+
conn.adapter Faraday.default_adapter
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def log_request(url, params)
|
|
69
|
+
return unless config.logger
|
|
70
|
+
|
|
71
|
+
config.logger.info("[AllinpayCnp] POST #{url}")
|
|
72
|
+
config.logger.debug("[AllinpayCnp] Params: #{params.reject { |k, _| k == :sign }}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def log_response(response)
|
|
76
|
+
return unless config.logger
|
|
77
|
+
|
|
78
|
+
config.logger.info("[AllinpayCnp] Response: #{response.status}")
|
|
79
|
+
config.logger.debug("[AllinpayCnp] Body: #{response.body}")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def log_error(message)
|
|
83
|
+
return unless config.logger
|
|
84
|
+
|
|
85
|
+
config.logger.error("[AllinpayCnp] Error: #{message}")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module AllinpayCnp
|
|
6
|
+
class Response
|
|
7
|
+
SUCCESS_CODE = '0000'
|
|
8
|
+
|
|
9
|
+
def initialize(http_response, error: nil, public_key: nil)
|
|
10
|
+
@http_response = http_response
|
|
11
|
+
@error = error
|
|
12
|
+
@public_key = public_key
|
|
13
|
+
@body = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def http_success?
|
|
17
|
+
return false if @error
|
|
18
|
+
|
|
19
|
+
@http_response&.success?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def success?
|
|
23
|
+
http_success? && result_code == SUCCESS_CODE
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def failure?
|
|
27
|
+
!success?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def body
|
|
31
|
+
@body ||= parse_body
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def raw_body
|
|
35
|
+
@http_response&.body
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def result_code
|
|
39
|
+
body['resultCode']
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def result_desc
|
|
43
|
+
body['resultDesc']
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def access_order_id
|
|
47
|
+
body['accessOrderId']
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def order_id
|
|
51
|
+
body['orderId']
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def payment_url
|
|
55
|
+
body['paymentUrl'] || body['counterUrl'] || body['url']
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def status
|
|
59
|
+
body['status']
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def status_desc
|
|
63
|
+
body['statusDesc']
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def amount
|
|
67
|
+
body['amount']
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def currency
|
|
71
|
+
body['currency']
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def refund_amount
|
|
75
|
+
body['refundAmount']
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def trans_time
|
|
79
|
+
time_str = body['transTime']
|
|
80
|
+
return nil unless time_str
|
|
81
|
+
|
|
82
|
+
Time.strptime(time_str, '%Y%m%d%H%M%S')
|
|
83
|
+
rescue ArgumentError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def card_no
|
|
88
|
+
body['cardNo']
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def card_orgn
|
|
92
|
+
body['cardOrgn']
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def sign
|
|
96
|
+
body['sign']
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def valid_signature?
|
|
100
|
+
return false unless @public_key && sign
|
|
101
|
+
|
|
102
|
+
Signature.verify(body, @public_key)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def [](key)
|
|
106
|
+
body[key.to_s]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def to_h
|
|
110
|
+
{
|
|
111
|
+
success: success?,
|
|
112
|
+
result_code: result_code,
|
|
113
|
+
result_desc: result_desc,
|
|
114
|
+
access_order_id: access_order_id,
|
|
115
|
+
order_id: order_id,
|
|
116
|
+
payment_url: payment_url,
|
|
117
|
+
status: status,
|
|
118
|
+
amount: amount,
|
|
119
|
+
currency: currency,
|
|
120
|
+
raw: body
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
attr_reader :http_response, :error, :public_key
|
|
127
|
+
|
|
128
|
+
def parse_body
|
|
129
|
+
return { 'resultCode' => 'NETWORK_ERROR', 'resultDesc' => @error.message } if @error
|
|
130
|
+
return {} if @http_response.nil?
|
|
131
|
+
|
|
132
|
+
JSON.parse(@http_response.body)
|
|
133
|
+
rescue JSON::ParserError
|
|
134
|
+
{ 'resultCode' => 'PARSE_ERROR', 'resultDesc' => 'Invalid JSON', 'raw' => @http_response.body }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
6
|
+
module AllinpayCnp
|
|
7
|
+
module Signature
|
|
8
|
+
class << self
|
|
9
|
+
def sign(params, private_key)
|
|
10
|
+
sign_string = build_sign_string(params)
|
|
11
|
+
rsa_sign(sign_string, private_key)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def verify(params, public_key)
|
|
15
|
+
params = params.transform_keys(&:to_s)
|
|
16
|
+
signature = params.delete('sign')
|
|
17
|
+
return false if signature.nil? || signature.empty?
|
|
18
|
+
|
|
19
|
+
sign_string = build_sign_string(params)
|
|
20
|
+
rsa_verify(sign_string, signature, public_key)
|
|
21
|
+
rescue StandardError
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def build_sign_string(params)
|
|
26
|
+
params
|
|
27
|
+
.transform_keys(&:to_s)
|
|
28
|
+
.reject { |k, v| k == 'sign' || v.nil? || v.to_s.strip.empty? }
|
|
29
|
+
.sort_by { |k, _| k }
|
|
30
|
+
.map { |k, v| "#{k}=#{v.to_s.strip}" }
|
|
31
|
+
.join('&')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def rsa_sign(content, private_key_pem)
|
|
37
|
+
pkey = load_private_key(private_key_pem)
|
|
38
|
+
signature = pkey.sign(OpenSSL::Digest.new('SHA256'), content)
|
|
39
|
+
Base64.strict_encode64(signature)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def rsa_verify(content, signature, public_key_pem)
|
|
43
|
+
pkey = load_public_key(public_key_pem)
|
|
44
|
+
pkey.verify(
|
|
45
|
+
OpenSSL::Digest.new('SHA256'),
|
|
46
|
+
Base64.decode64(signature),
|
|
47
|
+
content
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def load_private_key(key_content)
|
|
52
|
+
if key_content.include?('-----BEGIN')
|
|
53
|
+
OpenSSL::PKey::RSA.new(key_content)
|
|
54
|
+
else
|
|
55
|
+
pem = '-----BEGIN PRIVATE KEY-----\n#{key_content}\n-----END PRIVATE KEY-----'
|
|
56
|
+
OpenSSL::PKey::RSA.new(pem)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def load_public_key(key_content)
|
|
61
|
+
if key_content.include?('-----BEGIN')
|
|
62
|
+
OpenSSL::PKey::RSA.new(key_content)
|
|
63
|
+
else
|
|
64
|
+
pem = "-----BEGIN PUBLIC KEY-----\n#{key_content}\n-----END PUBLIC KEY-----"
|
|
65
|
+
OpenSSL::PKey::RSA.new(pem)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/allinpay_cnp.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'allinpay_cnp/version'
|
|
4
|
+
require_relative 'allinpay_cnp/config'
|
|
5
|
+
require_relative 'allinpay_cnp/signature'
|
|
6
|
+
require_relative 'allinpay_cnp/response'
|
|
7
|
+
require_relative 'allinpay_cnp/request'
|
|
8
|
+
require_relative 'allinpay_cnp/client'
|
|
9
|
+
|
|
10
|
+
module AllinpayCnp
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def config
|
|
15
|
+
@config ||= Config.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
if block_given?
|
|
20
|
+
yield(config)
|
|
21
|
+
end
|
|
22
|
+
config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def client
|
|
26
|
+
Client.new
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: allinpay_cnp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- DrinE
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '1.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
description: 通联支付 CNP 跨境信用卡收单 Ruby SDK
|
|
34
|
+
email:
|
|
35
|
+
- drine.liu@gmail.com
|
|
36
|
+
executables: []
|
|
37
|
+
extensions: []
|
|
38
|
+
extra_rdoc_files: []
|
|
39
|
+
files:
|
|
40
|
+
- README.md
|
|
41
|
+
- lib/allinpay_cnp.rb
|
|
42
|
+
- lib/allinpay_cnp/client.rb
|
|
43
|
+
- lib/allinpay_cnp/config.rb
|
|
44
|
+
- lib/allinpay_cnp/request.rb
|
|
45
|
+
- lib/allinpay_cnp/response.rb
|
|
46
|
+
- lib/allinpay_cnp/signature.rb
|
|
47
|
+
- lib/allinpay_cnp/version.rb
|
|
48
|
+
homepage: https://github.com/CoolDrinELiu/allinpay
|
|
49
|
+
licenses:
|
|
50
|
+
- MIT
|
|
51
|
+
metadata: {}
|
|
52
|
+
post_install_message:
|
|
53
|
+
rdoc_options: []
|
|
54
|
+
require_paths:
|
|
55
|
+
- lib
|
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 2.7.0
|
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
requirements: []
|
|
67
|
+
rubygems_version: 3.5.9
|
|
68
|
+
signing_key:
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: Ruby SDK for Allinpay CNP cross-border payment gateway
|
|
71
|
+
test_files: []
|