├── .rspec ├── .yardopts ├── .gitignore ├── lib ├── wechat-pay │ ├── version.rb │ ├── helper.rb │ ├── ecommerce.rb │ ├── ecommerce │ │ ├── subsidies.rb │ │ ├── bill.rb │ │ ├── balance.rb │ │ ├── refund.rb │ │ ├── combine_order.rb │ │ ├── order.rb │ │ ├── withdraw.rb │ │ ├── applyment.rb │ │ └── profitsharing.rb │ ├── sign.rb │ └── direct.rb └── wechat-pay.rb ├── .rubocop.yml ├── CHANGELOG.md ├── spec ├── spec_helper.rb ├── wechat-pay │ ├── direct_spec.rb │ ├── ecommerce_spec.rb │ └── sign_spec.rb └── fixtures │ ├── random_platform_cert.pem │ ├── random_apiclient_cert.pem │ ├── random_platform_key.pem │ └── random_apiclient_key.pem ├── Rakefile ├── Gemfile ├── wechat-pay.gemspec ├── .github └── workflows │ └── code_quality.yml └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --force-color 2 | --require spec_helper -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown 2 | --protected 3 | --no-private -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | Gemfile.lock 3 | doc/ 4 | .yardoc/ 5 | rdoc/ 6 | /*.pem 7 | -------------------------------------------------------------------------------- /lib/wechat-pay/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WechatPay 4 | VERSION = '1.1.0' 5 | end 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | SuggestExtensions: false 4 | 5 | Style/AsciiComments: 6 | Enabled: false 7 | 8 | Metrics/BlockLength: 9 | Enabled: false 10 | 11 | Metrics/AbcSize: 12 | Enabled: false 13 | 14 | Metrics/MethodLength: 15 | Max: 30 16 | 17 | Layout/LineLength: 18 | Enabled: false 19 | 20 | Metrics/ModuleLength: 21 | Max: 200 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Next Release 2 | 3 | ## 1.1.0 (07/26/2021) 4 | 5 | * Fix `Accept-Encoding` issue in wechat. 6 | 7 | 8 | ## 1.0.8 (05/26/2021) 9 | 10 | * Add api `invoke_transactions_in_native` to `WechatPay::Ecommerce` 11 | 12 | ## 1.0.7 (05/16/2021) 13 | 14 | * Change `WechatPay::Ecommerce.get_certificates` to `WechatPay::Ecommerce.certificates` 15 | * Refactor code and document. 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'openssl' 4 | require 'json' 5 | require 'wechat-pay' 6 | 7 | WechatPay.app_id = 'wxd930ea5d5a258f4f' 8 | WechatPay.mch_key = '8934e7d15453e97507ef794cf7b0519d' 9 | WechatPay.mch_id = '16000000' 10 | WechatPay.apiclient_key = File.read('spec/fixtures/random_apiclient_key.pem') 11 | WechatPay.apiclient_cert = File.read('spec/fixtures/random_apiclient_cert.pem') 12 | WechatPay.platform_cert = File.read('spec/fixtures/random_platform_cert.pem') 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'bundler/setup' 5 | require 'rspec/core/rake_task' 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | rescue LoadError 10 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 11 | end 12 | 13 | require 'rdoc/task' 14 | Rake::RDocTask.new do |rdoc| 15 | rdoc.rdoc_dir = 'rdoc' 16 | rdoc.title = "wechat-pay #{WechatPay::VERSION}" 17 | rdoc.rdoc_files.include('README*') 18 | rdoc.rdoc_files.include('lib/**/*.rb') 19 | end 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Declare your gem's dependencies in tenpay.gemspec. 6 | # Bundler will treat runtime dependencies like base dependencies, and 7 | # development dependencies will be added by default to the :development group. 8 | gemspec 9 | 10 | # Declare any dependencies that are still in development here instead of in 11 | # your gemspec. These might include edge Rails or gems from your path or 12 | # Git. Remember to move these dependencies to your gemspec before releasing 13 | # your gem to rubygems.org. 14 | 15 | # To use debugger 16 | -------------------------------------------------------------------------------- /spec/wechat-pay/direct_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'WechatPay::Direct' do 4 | %w[js miniprogram app h5 native].each do |key| 5 | it "check method invoke_transactions_in_#{key}" do 6 | result = { code: 200 } 7 | expect(WechatPay::Direct).to receive(:make_request).with(any_args).and_return(result) 8 | expect(WechatPay::Direct.send("invoke_transactions_in_#{key}", {})).to eq(result) 9 | end 10 | end 11 | 12 | %w[native miniprogram app h5 js].each do |key| 13 | it "check method invoke_combine_transactions_in_#{key}" do 14 | result = { code: 200 } 15 | expect(WechatPay::Ecommerce).to receive(:make_request).with(any_args).and_return(result) 16 | expect(WechatPay::Direct.const_get("invoke_combine_transactions_in_#{key}_fields".upcase).is_a?(Array)).to be(true) 17 | expect(WechatPay::Direct.send("invoke_combine_transactions_in_#{key}", {})).to eq(result) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /wechat-pay.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.push File.expand_path('lib', __dir__) 4 | 5 | require 'wechat-pay/version' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = 'wechat-pay' 9 | s.version = WechatPay::VERSION 10 | s.summary = 'Wechat Pay in api V3' 11 | s.description = 'A simple Wechat pay ruby gem in api V3.' 12 | s.authors = ['lanzhiheng'] 13 | s.email = 'lanzhihengrj@gmail.com' 14 | s.files = `git ls-files -z`.split("\x0").reject do |f| 15 | f.match(%r{^(test|spec|features)/}) 16 | end 17 | s.required_ruby_version = '>= 2.6' 18 | s.require_paths = ['lib'] 19 | s.homepage = 'https://github.com/lanzhiheng/wechat-pay/' 20 | s.license = 'MIT' 21 | 22 | s.add_runtime_dependency 'rest-client', '~> 2.0', '>= 2.0.0' 23 | s.add_development_dependency 'rake', '~> 13.0', '>= 13.0.3' 24 | s.add_development_dependency 'rspec', '~> 3.10.0' 25 | s.add_development_dependency 'rubocop', '~> 1.14.0' 26 | end 27 | -------------------------------------------------------------------------------- /lib/wechat-pay/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/concern' 4 | 5 | module WechatPayHelper # :nodoc: 6 | GATEWAY_URL = 'https://api.mch.weixin.qq.com' 7 | 8 | extend ActiveSupport::Concern 9 | 10 | class_methods do 11 | def build_query(params) 12 | params.sort.map { |key, value| "#{key}=#{value}" }.join('&') 13 | end 14 | 15 | def make_request(method:, path:, for_sign: '', payload: {}, extra_headers: {}) 16 | authorization = WechatPay::Sign.build_authorization_header(method, path, for_sign) 17 | headers = { 18 | 'Authorization' => authorization, 19 | 'Content-Type' => 'application/json', 20 | 'Accept-Encoding' => '*' 21 | }.merge(extra_headers) 22 | 23 | RestClient::Request.execute( 24 | url: "#{GATEWAY_URL}#{path}", 25 | method: method.downcase, 26 | payload: payload, 27 | headers: headers.compact # Remove empty items 28 | ) 29 | rescue ::RestClient::ExceptionWithResponse => e 30 | e.response 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/wechat-pay.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Naming/FileName 2 | 3 | # frozen_string_literal: true 4 | 5 | require 'restclient' 6 | require 'wechat-pay/sign' 7 | require 'wechat-pay/direct' # 直连模式 8 | require 'wechat-pay/ecommerce' # 电商平台 9 | 10 | # # 微信支付 11 | # 12 | # 设置关键信息 13 | module WechatPay 14 | class << self 15 | attr_accessor :app_id, :mch_id, :mch_key 16 | attr_reader :apiclient_key, :apiclient_cert, :platform_cert 17 | 18 | # 设置商户私钥,从微信商户平台下载 19 | def apiclient_key=(key) 20 | @apiclient_key = OpenSSL::PKey::RSA.new(key) 21 | end 22 | 23 | # 设置平台证书,通过接口获取 https://github.com/lanzhiheng/wechat-pay/blob/master/lib/wechat-pay/ecommerce/applyment.rb#L116 24 | def platform_cert=(cert) 25 | @platform_cert = OpenSSL::X509::Certificate.new(cert) 26 | end 27 | 28 | # 设置商户证书,从微信商户平台下载 29 | def apiclient_cert=(cert) 30 | @apiclient_cert = OpenSSL::X509::Certificate.new(cert) 31 | end 32 | 33 | # 平台证书序列号 34 | def platform_serial_no 35 | @platform_serial_no ||= platform_cert.serial.to_s(16) 36 | end 37 | 38 | # 商户证书序列号 39 | def apiclient_serial_no 40 | @apiclient_serial_no ||= apiclient_cert.serial.to_s(16) 41 | end 42 | end 43 | end 44 | # rubocop:enable Naming/FileName 45 | -------------------------------------------------------------------------------- /.github/workflows/code_quality.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | pull_request: 12 | branches: [ master ] 13 | 14 | jobs: 15 | test: 16 | 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | ruby-version: ['2.6', '2.7', '3.0'] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Set up Ruby 25 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 26 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 27 | # uses: ruby/setup-ruby@v1 28 | uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e 29 | with: 30 | ruby-version: ${{ matrix.ruby-version }} 31 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 32 | - name: Run rubocop 33 | run: bundle exec rubocop 34 | - name: Run tests 35 | run: bundle exec rake 36 | -------------------------------------------------------------------------------- /spec/fixtures/random_platform_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZTCCAk2gAwIBAgIEBf+LbjANBgkqhkiG9w0BAQsFADBCMRMwEQYKCZImiZPy 3 | LGQBGRYDb3JnMRkwFwYKCZImiZPyLGQBGRYJcnVieS1sYW5nMRAwDgYDVQQDDAdS 4 | dWJ5IENBMB4XDTIxMDQyNzExNTUyNloXDTIzMDQyNzExNTUyNlowQjETMBEGCgmS 5 | JomT8ixkARkWA29yZzEZMBcGCgmSJomT8ixkARkWCXJ1YnktbGFuZzEQMA4GA1UE 6 | AwwHUnVieSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMe2SjFV 7 | Z67F9L+ML0Shu48OqGqEbCfALjW2wNjLLKK+oVu3U/NZ+XKujgIMyis4CsF/lOEd 8 | e2evpemyxxJ345OOK6es4XWL87Cg6vSqwtrd0Cf+qRB8lCz1ptPFLzGa3QW0OupL 9 | T/KXO/r0Zji/eurvWTpwhIqsZYZomExH1UVh9pq9Y+YJrEP4cE1HITmKTHTGc+VZ 10 | 5tjqqhJqREklrWlq21kAgaM4pwIZpIJ7RCWRw4WqHhQNhJRJRonzAgo5v+9dF2Bc 11 | UokbPsYuFOj1d4XxnGnEX65DfT29gbPLFSam3Xyrei+444A6YYXaNX2JkCuDFCES 12 | R3xERDzuOP24SgcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E 13 | BAMCAQYwHQYDVR0OBBYEFHH9P2qC8LFE7Rx5vxn9Of89ESRxMB8GA1UdIwQYMBaA 14 | FHH9P2qC8LFE7Rx5vxn9Of89ESRxMA0GCSqGSIb3DQEBCwUAA4IBAQAzSAef025k 15 | Oo1pBIFz94gA//tiKkFdOHPLiph3TAO+Dop9x2GnKDLo75PxTck2HT2VVRl42w87 16 | +5xiyGV+hNQJaPHmWyWa+P9Y3BWMHNXLY6ScaxcsX5N8fwVsYyRgwcMSeOnjtd4q 17 | pRhaz5rEBnDYMr1uVbxkIwLXCcgN/vRr8AWD+qYhMu1866vEA36urzvegU7KVPvv 18 | pDxuJcnNg+44qf+hHxDPXFkmI+tiT8pWD0SHEY9mT/tHSt4ujdZku2xsK5OU2vFF 19 | giAyUOjrW+mvkPxzrPzQGaCb8AIAFiVMR4pEK2O/CfB3vkSrWsQmKRYuYjbdxoDv 20 | 6wncYGzLrl3A 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /spec/fixtures/random_apiclient_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZjCCAk6gAwIBAgIFAlSoAcAwDQYJKoZIhvcNAQELBQAwQjETMBEGCgmSJomT 3 | 8ixkARkWA29yZzEZMBcGCgmSJomT8ixkARkWCXJ1YnktbGFuZzEQMA4GA1UEAwwH 4 | UnVieSBDQTAeFw0yMTA0MjcxMTUxNTZaFw0yMzA0MjcxMTUxNTZaMEIxEzARBgoJ 5 | kiaJk/IsZAEZFgNvcmcxGTAXBgoJkiaJk/IsZAEZFglydWJ5LWxhbmcxEDAOBgNV 6 | BAMMB1J1YnkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+8XpP 7 | h8vgPvrSoicTd5f1fGbaL4nSj0z9t5dJmRMWwCoVup82ETJUl4BV+V/O+5Ucijbi 8 | 4lgWrmPxjk28jdGKaHBO+PGXzTcPybKTNM/A8jQzlZBAzbvx/m9c7Jw7NcP79fuQ 9 | ng0S6Oh2hS5dYRQq71kKSwsIce0Hd+Y+Jqyl7URTSRY+8c5Lp9A9NDnk6zAWYWy9 10 | +xE4MEB0IiVADCpOBloVDqEV/cCo8Uts1vU7W/dbZv5CPOkD6j31S6OXzsfmLf7u 11 | s5ik16gzwWoGni0YAAZjYnARUPZiVmu/ik3C4pks4r8awg/IpkJ5nc/wUNf8L0A1 12 | DeuCpyMof+CPuOZ1AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ 13 | BAQDAgEGMB0GA1UdDgQWBBS3rhd3GsQVpudgfupC8od1YBQTIjAfBgNVHSMEGDAW 14 | gBS3rhd3GsQVpudgfupC8od1YBQTIjANBgkqhkiG9w0BAQsFAAOCAQEAh6alNLX6 15 | FoHf5M6+6DUwfC3DremY7ROuJbaiFjiRaqQaKbe9VP/piZAQ1PO4WuGyYAJfXHYd 16 | lkw7431z0isEJjgoIf/qVR7ffrBJmZ8k5S+CEregO3j3/2QoiMFkAm8NG2rlyywq 17 | ElI8lRkiJQprIWYo3CJm+egdx9fjyfs/2y6Aj7bHGJG9ri0NhNwzzJ2eRiuTFTvD 18 | Yc4YRsmY3D0aZ9r5VnOdlsNnVoRL/3G9f5P7tA2Yli8uw4flbkiE4GLhjf77R+yR 19 | EKRuhu217NRZS8iJrA/drzX10/lLLjitxEG7Clhz5E2L5tpY1YxoBJ9VMf5Xp3hQ 20 | JQ3ErslGEsD5Qw== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /spec/wechat-pay/ecommerce_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'WechatPay::Ecommerce' do 6 | %w[js miniprogram app h5 native].each do |key| 7 | it "check method invoke_transactions_in_#{key}" do 8 | result = { code: 200 } 9 | expect(WechatPay::Ecommerce).to receive(:make_request).with(any_args).and_return(result) 10 | expect(WechatPay::Ecommerce.const_get("invoke_transactions_in_#{key}_fields".upcase).is_a?(Array)).to be(true) 11 | expect(WechatPay::Ecommerce.send("invoke_transactions_in_#{key}", {})).to eq(result) 12 | end 13 | end 14 | 15 | %w[native miniprogram app h5 js].each do |key| 16 | it "check method invoke_combine_transactions_in_#{key}" do 17 | result = { code: 200 } 18 | expect(WechatPay::Ecommerce).to receive(:make_request).with(any_args).and_return(result) 19 | expect(WechatPay::Ecommerce.const_get("invoke_combine_transactions_in_#{key}_fields".upcase).is_a?(Array)).to be(true) 20 | expect(WechatPay::Ecommerce.send("invoke_combine_transactions_in_#{key}", {})).to eq(result) 21 | end 22 | end 23 | 24 | it 'check media_video_upload method' do 25 | result = { code: 200 } 26 | f = File.open('spec/fixtures/random_apiclient_key.pem') 27 | expect(WechatPay::Ecommerce).to receive(:make_request).with(any_args).and_return(result) 28 | expect(WechatPay::Ecommerce.media_video_upload(f)).to eq(result) 29 | end 30 | 31 | it 'check media_upload method' do 32 | result = { code: 200 } 33 | f = File.open('spec/fixtures/random_apiclient_key.pem') 34 | expect(WechatPay::Ecommerce).to receive(:make_request).with(any_args).and_return(result) 35 | expect(WechatPay::Ecommerce.media_upload(f)).to eq(result) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/fixtures/random_platform_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAx7ZKMVVnrsX0v4wvRKG7jw6oaoRsJ8AuNbbA2Mssor6hW7dT 3 | 81n5cq6OAgzKKzgKwX+U4R17Z6+l6bLHEnfjk44rp6zhdYvzsKDq9KrC2t3QJ/6p 4 | EHyULPWm08UvMZrdBbQ66ktP8pc7+vRmOL966u9ZOnCEiqxlhmiYTEfVRWH2mr1j 5 | 5gmsQ/hwTUchOYpMdMZz5Vnm2OqqEmpESSWtaWrbWQCBozinAhmkgntEJZHDhaoe 6 | FA2ElElGifMCCjm/710XYFxSiRs+xi4U6PV3hfGcacRfrkN9Pb2Bs8sVJqbdfKt6 7 | L7jjgDphhdo1fYmQK4MUIRJHfEREPO44/bhKBwIDAQABAoIBADcjXwyL1dptEQup 8 | eotqU8xFcb4m3W2EI730vP2d6q7sDsSxst3nI3XEN7TdLxwLlvyhastURnP0DMye 9 | 7VNuAkkE4YyjsIOxphBH/VabpryirQu9xZOlsYtQL0UcldEOPqOKhRGWxXXmx0qc 10 | G3TjeN5QQsRduFpJCqa3TgUReBJ1YcgsF24iv+6S21QwacaiELO9LHEnRboITn8E 11 | H4S78FdjWUG7r0/39HIWL/dodfsLHM5RvfEdnrWJ/A+hKgIIDIbAUlYGK6ijceYy 12 | FeB0LSV/5goljMj0rB+QitGH8GJmhJwl15/nQ4SLqCZLIodxd3ySJtn1AzARdQPP 13 | nrFuxAECgYEA/0VcgZJFzgUvcghqnxeJNucKYc9Wm5SdVVrC8ts/LM1diDaKgnV/ 14 | ziaDwtg8UIHetjvgMnISb4e83jUXdTFVeEf5l9AXV5G6ST5378DoXHsbO3kW3ED8 15 | UrbgFkeyMkD6eikdDPqpND6vKEczR7UqW/ajHt4NHsHyqkyaqDrK3wcCgYEAyEhO 16 | o2ulG9QRNS2TrFYvgAlr0ZfezqE6Vh0PYrwXfzuJVwY1IEMnK4MCB9fMKSfNFkQt 17 | olxK0yEx+h8TnGv6HfYnlC/bA4ddahbWF0/OqRiho+OyBxWJpeWe/eKCEPWXdmOQ 18 | 3QLSdXLZK8303+F/2Frh4/FUX6LohTxoPkMBfQECgYEAu64LbVhV6jr1vylg+scb 19 | IzqK7465ZnnFk1O/sT5xHEeBVPyEqZYp+S9oAIFrFuXlEKbFF1G3LDjoK5dtP8Sd 20 | ymlgoLVl9AQ4qlE7bRKvxA7e3sMQg69j1IyQBNGBumD7x4UizsAcV0UfEsYGddpE 21 | 4ohbNf6cNtjxyTO5IabYMVECgYAurXt2Zt4iMDiadjbWkXeclZWFUanh6n2YGEm/ 22 | ryqiwpNtrsqu7Dey0mOkxEyWwunvaJBiKLRfpHrrWlbNu/SdCwOKa+TVW7UPxqa6 23 | 5CS8EDuL4MNbF0/vVCbL8QBzR2m3c9kNSV0Xdl7a8LNDgmCzYesHnvUVHPioJL3+ 24 | 1MsCAQKBgCkyKcsQhrii51otXF4N6c4ej3mqpq3pIm8ZSkskhKLxyBon+WawO58O 25 | EMeq7Hrv10qNOjELU4wTcuqlWeT259LwfsySw9mM4lTV+H0064tLK20JVGQU90P4 26 | 30545RaoJaEci/MZu39+FJNXuLgIgKNijowAAwwp7avoOubuXc/2 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/fixtures/random_apiclient_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAvvF6T4fL4D760qInE3eX9Xxm2i+J0o9M/beXSZkTFsAqFbqf 3 | NhEyVJeAVflfzvuVHIo24uJYFq5j8Y5NvI3RimhwTvjxl803D8mykzTPwPI0M5WQ 4 | QM278f5vXOycOzXD+/X7kJ4NEujodoUuXWEUKu9ZCksLCHHtB3fmPiaspe1EU0kW 5 | PvHOS6fQPTQ55OswFmFsvfsRODBAdCIlQAwqTgZaFQ6hFf3AqPFLbNb1O1v3W2b+ 6 | QjzpA+o99Uujl87H5i3+7rOYpNeoM8FqBp4tGAAGY2JwEVD2YlZrv4pNwuKZLOK/ 7 | GsIPyKZCeZ3P8FDX/C9ANQ3rgqcjKH/gj7jmdQIDAQABAoIBAQCQzgbI6540yO5k 8 | 8O4beFX4qMhDbUvjMCPeQe3stbbhSQhhhC8bzLzTpDWCfeUnzmmNxE/NjoPpZ4WJ 9 | +jZ/6Tlg8sVBTs/BJLM+OONBegqYM9ZczG8ihiOjaSbBXPs6eBLSMQD/8qzNi25H 10 | +8ZmsKmfyfZHtRN/6w4r3MTym1fRWKgkXyHjhgOwS+Hy0PCaRO6Z448mMOlRPxDW 11 | h5T0ttkCEbbHIDmMTGtQNZaqd1/5LmVrMsTouyj6COYQaW6A69xkiZhuE7zKsH4F 12 | BwMRBJmtRF4QK+P3lY2dlITRGhfaxXd5DzElnnvd6UkOf0r8VuIAuDLrWPkn017h 13 | 4H0XJG7hAoGBAOWfQw790LZAKxrXPjRMpKmjZqlnVEgE+wSeMtI/156ecQcgI3DX 14 | 3RGsbjcGt08H+BWSUIk35kUKfnbZKNK6dn1FAO7guqDLnse+OYXg2kuAeuDgxUdm 15 | Fbg+pQBUu8QqdeLLy3VkzXrdc5fKymhZswtqpx5eAautHBzwSVibTlVJAoGBANTg 16 | vx2sJzOhhu0wK/luxdFP0xIdusyhMOgYlYXL5l5C6cG8X4lUCMFCGD9fLMwFLN5K 17 | ShqNvXzzaSE3aC1QhnYxafs966TKJkNnQKz8yC22CySXMQxHxZuRsrHoa3uOFdkZ 18 | HYo5vEP3egVdKHUUh0cs0S11GcsaQdfccHO2yMPNAoGANc2HbO/UA6AteXCNxrte 19 | qdD7sR3hBa8FEiPvTIxg/W2qljzVkQ9DYWzBtmsAcKgxXPyXmk9ayTqYP0jK4/WE 20 | 5f1RJqfJkvujDLJp0BDLlX1ZTW/dScmFtVIYX2d7R4+bZ7TQy4T/EJbrCtoday35 21 | YedvmRH12kAJok47IWPiiuECgYAfDl6zXH8nmCQQDFwN+qwfWi7n0LCE0+tHoPaH 22 | W3TTQZ3KpsmlRj40u4jADgmCBitCjsH617zSMsyejO/E1J+ZNKJKhgEPvHISmUil 23 | NAecK5e6kdgU+4+Hn5zbOZYco2DqmDBoDv45SCxkBfA2DHWj25T0tcW6jK0Yac96 24 | AiuN7QKBgQC30pAAXBOZwfGIaI3COqhwS7xWVsVWAMXXNBQLqjrmLXP77rg74RXu 25 | hcXDPzKlrTVD41MNjar0jpTn1H43SVB8Uz77vgctS6wqt8dLs5XcywwI+mH9H7LL 26 | lLpyL95dbDwYMWZ7HVOXuClF8LgHHbBH4uQMRJzyDW24IgPnZsZTWw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /lib/wechat-pay/ecommerce.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'wechat-pay/helper' 5 | require 'wechat-pay/ecommerce/withdraw' 6 | require 'wechat-pay/ecommerce/balance' 7 | require 'wechat-pay/ecommerce/applyment' 8 | require 'wechat-pay/ecommerce/order' 9 | require 'wechat-pay/ecommerce/combine_order' 10 | require 'wechat-pay/ecommerce/profitsharing' 11 | require 'wechat-pay/ecommerce/subsidies' 12 | require 'wechat-pay/ecommerce/refund' 13 | require 'wechat-pay/ecommerce/bill' 14 | 15 | module WechatPay 16 | # # 服务商相关接口封装(电商平台,服务商有许多接口共用) 17 | # 文档: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/open/pay/chapter3_3_3.shtml 18 | # 19 | # PS: 电商收付通的所有接口已经封装完毕,有需要再补充。 20 | # 21 | module Ecommerce 22 | include WechatPayHelper 23 | 24 | # 视频上传 25 | # 26 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter2_1_2.shtml 27 | # 28 | # Example: 29 | # 30 | # ``` ruby 31 | # WechatPay::Ecommerce.media_video_upload(File.open('Your Video')) 32 | # ``` 33 | # 34 | def self.media_video_upload(video) 35 | url = '/v3/merchant/media/video_upload' 36 | method = 'POST' 37 | meta = { 38 | filename: video.to_path, 39 | sha256: Digest::SHA256.hexdigest(video.read) 40 | } 41 | 42 | video.rewind 43 | payload = { 44 | meta: meta.to_json, 45 | file: video 46 | } 47 | 48 | make_request( 49 | method: method, 50 | path: url, 51 | for_sign: meta.to_json, 52 | payload: payload, 53 | extra_headers: { 54 | 'Content-Type' => nil 55 | } 56 | ) 57 | end 58 | 59 | # 图片上传 60 | # 61 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter2_1_1.shtml 62 | # 63 | # Example: 64 | # 65 | # ``` ruby 66 | # WechatPay::Ecommerce.media_upload(File.open('Your Image')) 67 | # ``` 68 | def self.media_upload(image) 69 | url = '/v3/merchant/media/upload' 70 | method = 'POST' 71 | meta = { 72 | filename: image.to_path, 73 | sha256: Digest::SHA256.hexdigest(image.read) 74 | } 75 | 76 | image.rewind 77 | payload = { 78 | meta: meta.to_json, 79 | file: image 80 | } 81 | 82 | make_request( 83 | method: method, 84 | path: url, 85 | for_sign: meta.to_json, 86 | payload: payload, 87 | extra_headers: { 88 | 'Content-Type' => nil # Pass nil to remove the Content-Type 89 | } 90 | ) 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/wechat-pay/ecommerce/subsidies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WechatPay 4 | # 补差相关 5 | module Ecommerce 6 | REQUEST_SUBSIDIES_FIELDS = %i[sub_mchid transaction_id amount description].freeze # :nodoc: 7 | # 8 | # 请求补差 9 | # 10 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_5_1.shtml 11 | # 12 | # Example: 13 | # 14 | # ``` ruby 15 | # WechatPay::Ecommerce.request_subsidies(sub_mchid: '16000000', transaction_id: '4323400972202104305131070170', amount: 1, description: '订单补差') 16 | # ``` 17 | # 18 | def self.request_subsidies(params) 19 | url = '/v3/ecommerce/subsidies/create' 20 | method = 'POST' 21 | 22 | payload_json = params.to_json 23 | 24 | make_request( 25 | method: method, 26 | path: url, 27 | for_sign: payload_json, 28 | payload: payload_json 29 | ) 30 | end 31 | 32 | RETURN_SUBSIDIES_FIELDS = %i[sub_mchid transaction_id amount description out_order_no].freeze # :nodoc: 33 | # 34 | # 补差回退 35 | # 36 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_5_2.shtml 37 | # 38 | # Example: 39 | # 40 | # ``` ruby 41 | # WechatPay::Ecommerce.return_subsidies(sub_mchid: '16000000', transaction_id: '4323400972202104305131070170', amount: 1, description: '订单补差', out_order_no: 'P103333') 42 | # ``` 43 | 44 | def self.return_subsidies(params) 45 | url = '/v3/ecommerce/subsidies/return' 46 | method = 'POST' 47 | 48 | payload_json = params.to_json 49 | 50 | make_request( 51 | method: method, 52 | path: url, 53 | for_sign: payload_json, 54 | payload: payload_json 55 | ) 56 | end 57 | 58 | CANCEL_SUBSIDIES_FIELDS = %i[sub_mchid transaction_id description].freeze # :nodoc: 59 | # 60 | # 取消补差 61 | # 62 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_5_3.shtml 63 | # 64 | # Example: 65 | # 66 | # ``` ruby 67 | # WechatPay::Ecommerce.return_subsidies(sub_mchid: '1600000', transaction_id: '4323400972202104305131070170', amount: 1, description: '订单补差', out_order_no: 'P103333') 68 | # ``` 69 | # 70 | def self.cancel_subsidies(params) 71 | url = '/v3/ecommerce/subsidies/cancel' 72 | method = 'POST' 73 | 74 | payload_json = params.to_json 75 | 76 | make_request( 77 | method: method, 78 | path: url, 79 | for_sign: payload_json, 80 | payload: payload_json 81 | ) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/wechat-pay/ecommerce/bill.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WechatPay 4 | # 账单相关 5 | module Ecommerce 6 | class << self 7 | TRADEBILL_FIELDS = [:bill_date].freeze # :nodoc: 8 | # 9 | # 申请交易账单 10 | # 11 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_9_1.shtml 12 | # 13 | # Example: 14 | # 15 | # ``` ruby 16 | # WechatPay::Ecommerce.tradebill(bill_date: '2021-04-30') 17 | # ``` 18 | def tradebill(params) 19 | path = '/v3/bill/tradebill' 20 | method = 'GET' 21 | 22 | query = build_query(params) 23 | url = "#{path}?#{query}" 24 | 25 | make_request( 26 | path: url, 27 | method: method, 28 | extra_headers: { 29 | 'Content-Type' => 'application/x-www-form-urlencoded' 30 | } 31 | ) 32 | end 33 | 34 | FUNDFLOWBILL_FIELDS = [:bill_date].freeze # :nodoc: 35 | # 36 | # 申请资金账单 37 | # 38 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_9_2.shtml 39 | # 40 | # Example: 41 | # 42 | # ``` ruby 43 | # WechatPay::Ecommerce.fundflowbill(bill_date: '2021-04-30') 44 | # ``` 45 | # 46 | def fundflowbill(params) 47 | path = '/v3/bill/fundflowbill' 48 | method = 'GET' 49 | 50 | query = build_query(params) 51 | url = "#{path}?#{query}" 52 | 53 | make_request( 54 | path: url, 55 | method: method, 56 | extra_headers: { 57 | 'Content-Type' => 'application/x-www-form-urlencoded' 58 | } 59 | ) 60 | end 61 | 62 | ECOMMERCE_FUNDFLOWBILL_FIELDS = %i[bill_date account_type algorithm].freeze # :nodoc: 63 | # 64 | # 二级商户资金账单 65 | # 66 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_9_5.shtml 67 | # 68 | # Example: 69 | # 70 | # ``` ruby 71 | # WechatPay::Ecommerce.ecommerce_fundflowbill(bill_date: '2021-04-30', account_type: 'ALL', algorithm: 'AEAD_AES_256_GCM') 72 | # ``` 73 | # 74 | def ecommerce_fundflowbill(params) 75 | path = '/v3/ecommerce/bill/fundflowbill' 76 | method = 'GET' 77 | 78 | query = build_query(params) 79 | url = "#{path}?#{query}" 80 | 81 | make_request( 82 | path: url, 83 | method: method, 84 | extra_headers: { 85 | 'Content-Type' => 'application/x-www-form-urlencoded' 86 | } 87 | ) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wechat Pay 2 | 3 | A simple Wechat pay ruby gem, without unnecessary magic or wrapper. Just a simple wrapper for api V3. Refer to [wx_pay](https://github.com/jasl/wx_pay) 4 | 5 | Please read official document first: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/pages/index.shtml 6 | 7 | If you want check the present public api, you can find them in the [Document](https://www.rubydoc.info/github/lanzhiheng/wechat-pay/master/index)。 8 | 9 | Summary: 10 | 11 | `WechatPay::Direct` will contain the public api for direct connection merchant(直连商户)and `WechatPay::Ecommerce` will contain the public api for ecommerce(服务商,电商平台)。For more detail you can refer to the wechat document. 12 | 13 | - 直连商户: https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml 14 | - 服务商: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/index.shtml 15 | - 电商平台(电商收付通): https://pay.weixin.qq.com/wiki/doc/apiv3_partner/open/pay/chapter3_3_3.shtml 16 | 17 | If you find any issue in this repo, don't shy to create issues https://github.com/lanzhiheng/wechat-pay/issues 18 | 19 | For more Information,you can check my posts: https://www.lanzhiheng.com/posts/preview/ruby-gem-for-wechat-pay-v3 20 | 21 | # Installation 22 | 23 | Add this line to your Gemfile: 24 | 25 | ``` 26 | gem 'wechat-pay' 27 | ``` 28 | 29 | or development version 30 | 31 | ``` 32 | gem 'wechat-pay', :github => 'lanzhiheng/wechat-pay' 33 | ``` 34 | 35 | And then execute: 36 | 37 | ``` 38 | $ bundle 39 | ``` 40 | 41 | # Usage 42 | 43 | ## Configuration 44 | 45 | Create `config/initializer/wechat_pay.rb`and put following configurations into it 46 | 47 | ``` ruby 48 | WechatPay.apiclient_key = File.read('apiclient_key.pem') 49 | WechatPay.platform_cert = File.read('platform_cert.pem') # You should comment this line before downloaded platform_cert. 50 | WechatPay.apiclient_cert = File.read('apiclient_cert.pem') 51 | WechatPay.app_id = 'Your App Id' 52 | WechatPay.mch_id = 'Your Mch Id' 53 | WechatPay.mch_key = 'Your Mch Key' 54 | ``` 55 | 56 | ## Download 57 | 58 | I will provide a simple script for you to download the platform_cert 59 | 60 | ``` ruby 61 | def download_certificate 62 | download_path = 'Your Download Path' 63 | raise '必须提供证书下载路径' if download_path.blank? 64 | 65 | response = WechatPay::Ecommerce.certificates 66 | 67 | raise '证书下载失败' unless response.code == 200 68 | 69 | result = JSON.parse(response.body) 70 | # 需要按生效日期进行排序,获取最新的证书 71 | array = result['data'].sort_by { |item| -Time.parse(item['effective_time']).to_i } 72 | current_data = array.first 73 | encrypt_certificate = current_data['encrypt_certificate'] 74 | associated_data = encrypt_certificate['associated_data'] 75 | nonce = encrypt_certificate['nonce'] 76 | ciphertext = encrypt_certificate['ciphertext'] 77 | 78 | content = WechatPay::Sign.decrypt_the_encrypt_params( 79 | associated_data: associated_data, 80 | nonce: nonce, 81 | ciphertext: ciphertext 82 | ) 83 | 84 | File.open(download_path, 'w') do |f| 85 | f.write(content) 86 | end 87 | 88 | puts '证书下载成功' 89 | end 90 | ``` 91 | 92 | 93 | -------------------------------------------------------------------------------- /lib/wechat-pay/ecommerce/balance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WechatPay 4 | # 余额相关 5 | module Ecommerce 6 | QUERY_REALTIME_BALANCE_FIELDS = [:sub_mchid].freeze # :nodoc: 7 | # 8 | # 余额实时查询 9 | # 10 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_7_1.shtml 11 | # 12 | # Example: 13 | # 14 | # ``` ruby 15 | # WechatPay::Ecommerce.query_realtime_balance(sub_mchid: '1600000') 16 | # ``` 17 | def self.query_realtime_balance(params) 18 | sub_mchid = params.delete(:sub_mchid) 19 | url = "/v3/ecommerce/fund/balance/#{sub_mchid}" 20 | method = 'GET' 21 | 22 | make_request( 23 | path: url, 24 | method: method, 25 | extra_headers: { 26 | 'Content-Type' => 'application/x-www-form-urlencoded' 27 | } 28 | ) 29 | end 30 | 31 | QUERY_ENDDAY_BALANCE_FIELDS = %i[sub_mchid date].freeze # :nodoc: 32 | # 33 | # 日终余额查询 34 | # 35 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_7_2.shtml 36 | # 37 | # Example: 38 | # 39 | # ``` ruby 40 | # WechatPay::Ecommerce.query_endday_balance(sub_mchid: '1600000', date: '2019-08-17') 41 | # ``` 42 | def self.query_endday_balance(params) 43 | sub_mchid = params.delete(:sub_mchid) 44 | path = "/v3/ecommerce/fund/balance/#{sub_mchid}" 45 | method = 'GET' 46 | query = build_query(params) 47 | url = "#{path}?#{query}" 48 | 49 | make_request( 50 | path: url, 51 | method: method, 52 | extra_headers: { 53 | 'Content-Type' => 'application/x-www-form-urlencoded' 54 | } 55 | ) 56 | end 57 | 58 | QUERY_PLATFORM_REALTIME_BALANCE_FIELDS = %i[account_type].freeze # :nodoc: 59 | # 60 | # 平台商户实时余额 61 | # 62 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_7_3.shtml 63 | # 64 | # Example: 65 | # 66 | # ``` ruby 67 | # WechatPay::Ecommerce.query_platform_realtime_balance(account_type: 'BASIC') # basic account 68 | # WechatPay::Ecommerce.query_platform_realtime_balance(account_type: 'FEES') # fees account 69 | # ``` 70 | # 71 | def self.query_platform_realtime_balance(params) 72 | account_type = params.delete(:account_type) 73 | url = "/v3/merchant/fund/balance/#{account_type}" 74 | method = 'GET' 75 | 76 | make_request( 77 | path: url, 78 | method: method, 79 | extra_headers: { 80 | 'Content-Type' => 'application/x-www-form-urlencoded' 81 | } 82 | ) 83 | end 84 | 85 | QUERY_PLATFORM_ENDDAY_BALANCE_FIELDS = %i[account_type date].freeze # :nodoc: 86 | # 87 | # 平台商户日终余额 88 | # 89 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_7_4.shtml 90 | # 91 | # Example: 92 | # 93 | # ``` ruby 94 | # WechatPay::Ecommerce.query_platform_endday_balance(account_type: 'BASIC') 95 | # WechatPay::Ecommerce.query_platform_endday_balance(account_type: 'FEES') 96 | # ``` 97 | # 98 | def self.query_platform_endday_balance(params) 99 | account_type = params.delete(:account_type) 100 | path = "/v3/merchant/fund/dayendbalance/#{account_type}" 101 | method = 'GET' 102 | query = build_query(params) 103 | url = "#{path}?#{query}" 104 | 105 | make_request( 106 | path: url, 107 | method: method, 108 | extra_headers: { 109 | 'Content-Type' => 'application/x-www-form-urlencoded' 110 | } 111 | ) 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/wechat-pay/ecommerce/refund.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WechatPay 4 | # 退款相关 5 | module Ecommerce 6 | INVOKE_REFUND_FIELDS = %i[sub_mchid out_trade_no total refund out_refund_no].freeze # :nodoc: 7 | # 8 | # 退款申请 9 | # 10 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_6_1.shtml 11 | # 12 | # Example: 13 | # 14 | # ``` ruby 15 | # WechatPay::Ecommerce.invoke_refund(sub_mchid: '1600000', transaction_id: '4323400972202104305131070170', total: 1, refund: 1, description: '退款', out_refund_no: 'R10000') # by transaction_id 16 | # WechatPay::Ecommerce.invoke_refund(sub_mchid: '1608977559', total: 1, refund: 1, description: '退款', out_trade_no: 'N202104302474', out_refund_no: 'R10000') # by out_trade_no 17 | # ``` 18 | def self.invoke_refund(params) 19 | url = '/v3/ecommerce/refunds/apply' 20 | method = 'POST' 21 | amount = { 22 | refund: params.delete(:refund), 23 | total: params.delete(:total), 24 | currency: 'CNY' 25 | } 26 | 27 | params = { 28 | amount: amount, 29 | sp_appid: WechatPay.app_id 30 | }.merge(params) 31 | 32 | make_request( 33 | path: url, 34 | method: method, 35 | for_sign: params.to_json, 36 | payload: params.to_json 37 | ) 38 | end 39 | 40 | QUERY_REFUND_FIELDS = %i[sub_mchid refund_id out_refund_no].freeze # :nodoc: 41 | # 42 | # 退款查询 43 | # 44 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_6_2.shtml 45 | # 46 | # Example: 47 | # 48 | # ``` ruby 49 | # WechatPay::Ecommerce.query_refund(sub_mchid: '16000000', out_refund_no: 'AFS202104302474') 50 | # WechatPay::Ecommerce.query_refund(sub_mchid: '16000000', refund_id: '50000000382019052709732678859') 51 | # ``` 52 | # 53 | def self.query_refund(params) 54 | if params[:refund_id] 55 | params.delete(:out_refund_no) 56 | refund_id = params.delete(:refund_id) 57 | path = "/v3/ecommerce/refunds/id/#{refund_id}" 58 | else 59 | params.delete(:refund_id) 60 | out_refund_no = params.delete(:out_refund_no) 61 | path = "/v3/ecommerce/refunds/out-refund-no/#{out_refund_no}" 62 | end 63 | 64 | params = params.merge({ 65 | sp_mchid: WechatPay.mch_id 66 | }) 67 | 68 | method = 'GET' 69 | query = build_query(params) 70 | url = "#{path}?#{query}" 71 | 72 | make_request( 73 | method: method, 74 | path: url, 75 | extra_headers: { 76 | 'Content-Type' => 'application/x-www-form-urlencoded' 77 | } 78 | ) 79 | end 80 | 81 | RETURN_ADVANCE_REFUND_FIELDS = %i[refund_id sub_mchid].freeze # :nodoc: 82 | # 83 | # 垫付退款回补 84 | # 85 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_6_4.shtml 86 | # 87 | # Example: 88 | # 89 | # ``` ruby 90 | # WechatPay::Ecommerce.return_advance_refund(refund_id: '50300908092021043008398036516', sub_mchid: '160000') 91 | # ``` 92 | # 93 | def self.return_advance_refund(params) 94 | refund_id = params.delete(:refund_id) 95 | url = "/v3/ecommerce/refunds/#{refund_id}/return-advance" 96 | method = 'POST' 97 | 98 | make_request( 99 | path: url, 100 | method: method, 101 | for_sign: params.to_json, 102 | payload: params.to_json 103 | ) 104 | end 105 | 106 | QUERY_RETURN_ADVANCE_REFUND_FIELDS = %i[sub_mchid refund_id].freeze # :nodoc: 107 | # 108 | # 退款查询 109 | # 110 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_6_2.shtml 111 | # 112 | # Example: 113 | # 114 | # ``` ruby 115 | # WechatPay::Ecommerce.query_refund(sub_mchid: '16000000', out_refund_no: 'AFS202104302474') 116 | # WechatPay::Ecommerce.query_refund(sub_mchid: '16000000', refund_id: '50000000382019052709732678859') 117 | # ``` 118 | # 119 | def self.query_return_advance_refund(params) 120 | refund_id = params.delete(:refund_id) 121 | path = "/v3/ecommerce/refunds/#{refund_id}/return-advance" 122 | method = 'GET' 123 | query = build_query(params) 124 | url = "#{path}?#{query}" 125 | 126 | make_request( 127 | method: method, 128 | path: url, 129 | extra_headers: { 130 | 'Content-Type' => 'application/x-www-form-urlencoded' 131 | } 132 | ) 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/wechat-pay/ecommerce/combine_order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WechatPay 4 | # 合并订单相关 5 | module Ecommerce 6 | # @private 7 | # @!macro [attach] define_transaction_method 8 | # $1合单 9 | # 10 | # Document: $3 11 | # 12 | # Example: 13 | # 14 | # ``` ruby 15 | # params = { 16 | # combine_out_trade_no: 'combine_out_trade_no', 17 | # combine_payer_info: { 18 | # openid: 'client open id' 19 | # }, 20 | # sub_orders: [ 21 | # { 22 | # mchid: 'mchid', 23 | # sub_mchid: 'sub mchid', 24 | # attach: 'attach', 25 | # amount: { 26 | # total_amount: 100, 27 | # currency: 'CNY' 28 | # }, 29 | # out_trade_no: 'out_trade_no', 30 | # description: 'description' 31 | # } 32 | # ], 33 | # notify_url: 'the_url' 34 | # } 35 | # 36 | # WechatPay::Ecommerce.invoke_combine_transactions_in_$1(params) 37 | # ``` 38 | # @!method invoke_combine_transactions_in_$1 39 | # @!scope class 40 | def self.define_combine_transaction_method(key, value, _document) 41 | const_set("INVOKE_COMBINE_TRANSACTIONS_IN_#{key.upcase}_FIELDS", 42 | %i[combine_out_trade_no combine_payer_info sub_orders notify_url].freeze) 43 | define_singleton_method("invoke_combine_transactions_in_#{key}") do |params| 44 | combine_transactions_method_by_suffix(value, params) 45 | end 46 | end 47 | 48 | define_combine_transaction_method('js', 'jsapi', 'https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_3_3.shtml') 49 | define_combine_transaction_method('h5', 'h5', 'https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_3_2.shtml') 50 | define_combine_transaction_method('native', 'native', 'https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_3_5.shtml') 51 | define_combine_transaction_method('app', 'app', 'https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_3_1.shtml') 52 | define_combine_transaction_method('miniprogram', 'jsapi', 'https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_3_4.shtml') 53 | 54 | QUERY_COMBINE_ORDER_FIELDS = %i[combine_out_trade_no].freeze # :nodoc: 55 | # 56 | # 合单查询 57 | # 58 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_3_11.shtml 59 | # 60 | # ``` ruby 61 | # WechatPay::Ecommerce.query_order(combine_out_trade_no: 'C202104302474') 62 | # ``` 63 | # 64 | def self.query_combine_order(params) 65 | combine_out_trade_no = params.delete(:combine_out_trade_no) 66 | 67 | url = "/v3/combine-transactions/out-trade-no/#{combine_out_trade_no}" 68 | 69 | method = 'GET' 70 | 71 | make_request( 72 | method: method, 73 | path: url, 74 | extra_headers: { 75 | 'Content-Type' => 'application/x-www-form-urlencoded' 76 | } 77 | ) 78 | end 79 | 80 | CLOSE_COMBINE_ORDER_FIELDS = %i[combine_out_trade_no sub_orders].freeze # :nodoc: 81 | # 82 | # 关闭合单 83 | # 84 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_3_11.shtml 85 | # 86 | # ``` ruby 87 | # WechatPay::Ecommerce.close_combine_order(combine_out_trade_no: 'C202104302474') 88 | # ``` 89 | def self.close_combine_order(params) 90 | combine_out_trade_no = params.delete(:combine_out_trade_no) 91 | 92 | url = "/v3/combine-transactions/out-trade-no/#{combine_out_trade_no}/close" 93 | 94 | payload = { 95 | combine_appid: WechatPay.app_id 96 | }.merge(params) 97 | 98 | payload_json = payload.to_json 99 | 100 | method = 'POST' 101 | 102 | make_request( 103 | method: method, 104 | for_sign: payload_json, 105 | payload: payload_json, 106 | path: url 107 | ) 108 | end 109 | 110 | class << self 111 | private 112 | 113 | def combine_transactions_method_by_suffix(suffix, params) 114 | url = "/v3/combine-transactions/#{suffix}" 115 | method = 'POST' 116 | 117 | params = { 118 | combine_mchid: WechatPay.mch_id, 119 | combine_appid: WechatPay.app_id 120 | }.merge(params) 121 | 122 | payload_json = params.to_json 123 | 124 | make_request( 125 | method: method, 126 | path: url, 127 | for_sign: payload_json, 128 | payload: payload_json 129 | ) 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/wechat-pay/ecommerce/order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WechatPay 4 | # 订单相关 5 | module Ecommerce 6 | # @private 7 | # @!macro [attach] define_transaction_method 8 | # $1下单 9 | # 10 | # Document: $3 11 | # 12 | # Example: 13 | # 14 | # ``` 15 | # params = { 16 | # description: 'pay', 17 | # out_trade_no: 'Order Number', 18 | # payer: { 19 | # sp_openid: 'wechat open id' 20 | # }, 21 | # amount: { 22 | # total: 10 23 | # }, 24 | # sub_mchid: 'Your sub mchid', 25 | # notify_url: 'the url' 26 | # } 27 | # 28 | # WechatPay::Ecommerce.invoke_transactions_in_$1(params) 29 | # ``` 30 | # @!method invoke_transactions_in_$1 31 | # @!scope class 32 | def self.define_transaction_method(key, value, _document) 33 | const_set("INVOKE_TRANSACTIONS_IN_#{key.upcase}_FIELDS", 34 | %i[sub_mchid description out_trade_no notify_url amount].freeze) 35 | define_singleton_method("invoke_transactions_in_#{key}") do |params| 36 | transactions_method_by_suffix(value, params) 37 | end 38 | end 39 | 40 | define_transaction_method('native', 'native', 'document missing') 41 | define_transaction_method('js', 'jsapi', 'https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_2_2.shtml') 42 | define_transaction_method('app', 'app', 'https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_2_1.shtml') 43 | define_transaction_method('h5', 'h5', 'https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_2_4.shtml') 44 | define_transaction_method('miniprogram', 'jsapi', 'https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_2_3.shtml') 45 | 46 | QUERY_ORDER_FIELDS = %i[sub_mchid out_trade_no transaction_id].freeze # :nodoc: 47 | # 48 | # 订单查询 49 | # 50 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_2_5.shtml 51 | # 52 | # ``` ruby 53 | # WechatPay::Ecommerce.query_order(sub_mchid: '16000008', transaction_id: '4323400972202104305133344444') # by transaction_id 54 | # WechatPay::Ecommerce.query_order(sub_mchid: '16000008', out_trade_no: 'N202104302474') # by out_trade_no 55 | # ``` 56 | # 57 | def self.query_order(params) 58 | if params[:transaction_id] 59 | params.delete(:out_trade_no) 60 | transaction_id = params.delete(:transaction_id) 61 | path = "/v3/pay/partner/transactions/id/#{transaction_id}" 62 | else 63 | params.delete(:transaction_id) 64 | out_trade_no = params.delete(:out_trade_no) 65 | path = "/v3/pay/partner/transactions/out-trade-no/#{out_trade_no}" 66 | end 67 | 68 | params = params.merge({ 69 | sp_mchid: WechatPay.mch_id 70 | }) 71 | 72 | method = 'GET' 73 | query = build_query(params) 74 | url = "#{path}?#{query}" 75 | 76 | make_request( 77 | method: method, 78 | path: url, 79 | extra_headers: { 80 | 'Content-Type' => 'application/x-www-form-urlencoded' 81 | } 82 | ) 83 | end 84 | 85 | CLOSE_ORDER_FIELDS = %i[sub_mchid out_trade_no].freeze # :nodoc: 86 | # 87 | # 关闭订单 88 | # 89 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_2_6.shtml 90 | # 91 | # ``` ruby 92 | # WechatPay::Ecommerce.close_order(sub_mchid: '16000008', out_trade_no: 'N3344445') 93 | # ``` 94 | # 95 | def self.close_order(params) 96 | out_trade_no = params.delete(:out_trade_no) 97 | url = "/v3/pay/partner/transactions/out-trade-no/#{out_trade_no}/close" 98 | params = params.merge({ 99 | sp_mchid: WechatPay.mch_id 100 | }) 101 | 102 | method = 'POST' 103 | 104 | make_request( 105 | method: method, 106 | path: url, 107 | for_sign: params.to_json, 108 | payload: params.to_json 109 | ) 110 | end 111 | 112 | class << self 113 | private 114 | 115 | def transactions_method_by_suffix(suffix, params) 116 | url = "/v3/pay/partner/transactions/#{suffix}" 117 | method = 'POST' 118 | 119 | params = { 120 | sp_appid: WechatPay.app_id, 121 | sp_mchid: WechatPay.mch_id 122 | }.merge(params) 123 | 124 | payload_json = params.to_json 125 | 126 | make_request( 127 | method: method, 128 | path: url, 129 | for_sign: payload_json, 130 | payload: payload_json 131 | ) 132 | end 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/wechat-pay/ecommerce/withdraw.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WechatPay 4 | # 提现相关 5 | module Ecommerce 6 | WITHDRAW_FIELDS = %i[sub_mchid out_request_no amount].freeze # :nodoc: 7 | # 8 | # 二级商户提现 9 | # 10 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_8_2.shtml 11 | # 12 | # Example: 13 | # 14 | # ``` ruby 15 | # WechatPay::Ecommerce.withdraw(sub_mchid: '160000', out_request_no: 'P10000', amount: 1) 16 | # ``` 17 | # 18 | def self.withdraw(params) 19 | url = '/v3/ecommerce/fund/withdraw' 20 | method = 'POST' 21 | 22 | make_request( 23 | method: method, 24 | path: url, 25 | for_sign: params.to_json, 26 | payload: params.to_json 27 | ) 28 | end 29 | 30 | QUERY_WITHDRAW_FIELDS = %i[withdraw_id out_request_no sub_mchid].freeze # :nodoc: 31 | # 32 | # 二级商户提现查询 33 | # 34 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_8_3.shtml 35 | # 36 | # Example: 37 | # 38 | # ``` ruby 39 | # WechatPay::Ecommerce.query_withdraw(withdraw_id: '335556', sub_mchid: '160000') 40 | # WechatPay::Ecommerce.query_withdraw(out_request_no: 'P1000', sub_mchid: '160000') 41 | # ``` 42 | # 43 | def self.query_withdraw(params) 44 | if params[:withdraw_id] 45 | params.delete(:out_request_no) 46 | withdraw_id = params.delete(:withdraw_id) 47 | path = "/v3/ecommerce/fund/withdraw/#{withdraw_id}" 48 | else 49 | params.delete(:withdraw_id) 50 | out_request_no = params.delete(:out_request_no) 51 | path = "/v3/ecommerce/fund/withdraw/out-request-no/#{out_request_no}" 52 | end 53 | 54 | method = 'GET' 55 | query = build_query(params) 56 | url = "#{path}?#{query}" 57 | 58 | make_request( 59 | method: method, 60 | path: url, 61 | extra_headers: { 62 | 'Content-Type' => 'application/x-www-form-urlencoded' 63 | } 64 | ) 65 | end 66 | 67 | PLATFORM_WITHDRAW_FIELDS = %i[out_request_no amount account_type].freeze # :nodoc: 68 | # 69 | # 电商平台提现 70 | # 71 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_8_5.shtml 72 | # 73 | # Example: 74 | # 75 | # ``` ruby 76 | # WechatPay::Ecommerce.platform_withdraw(out_request_no: 'P10000', amount: 1, account_type: 'BASIC') 77 | # WechatPay::Ecommerce.platform_withdraw(out_request_no: 'P10000', amount: 1, account_type: 'FEES') 78 | # ``` 79 | # 80 | def self.platform_withdraw(params) 81 | url = '/v3/merchant/fund/withdraw' 82 | method = 'POST' 83 | 84 | make_request( 85 | method: method, 86 | path: url, 87 | for_sign: params.to_json, 88 | payload: params.to_json 89 | ) 90 | end 91 | 92 | QUERY_PLATFORM_WITHDRAW_FIELDS = %i[withdraw_id out_request_no].freeze # :nodoc: 93 | # 94 | # 商户平台提现查询 95 | # 96 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_8_6.shtml 97 | # 98 | # Example: 99 | # 100 | # ``` ruby 101 | # WechatPay::Ecommerce.query_platform_withdraw(out_request_no: 'P1000') 102 | # WechatPay::Ecommerce.query_platform_withdraw(withdraw_id: '12313153') 103 | # ``` 104 | # 105 | def self.query_platform_withdraw(params) 106 | if params[:withdraw_id] 107 | params.delete(:out_request_no) 108 | withdraw_id = params.delete(:withdraw_id) 109 | url = "/v3/merchant/fund/withdraw/withdraw-id/#{withdraw_id}" 110 | else 111 | params.delete(:withdraw_id) 112 | out_request_no = params.delete(:out_request_no) 113 | url = "/v3/merchant/fund/withdraw/out-request-no/#{out_request_no}" 114 | end 115 | 116 | method = 'GET' 117 | 118 | make_request( 119 | method: method, 120 | path: url, 121 | extra_headers: { 122 | 'Content-Type' => 'application/x-www-form-urlencoded' 123 | } 124 | ) 125 | end 126 | 127 | DOWNLOAD_EXCEPTION_WITHDRAW_FILE = %i[bill_type bill_date].freeze # :nodoc: 128 | # 129 | # 按日下载提现异常文件 130 | # 131 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_8_4.shtml 132 | # 133 | # ``` ruby 134 | # WechatPay::Ecommerce.download_exception_withdraw_file(bill_type: 'NO_SUCC', bill_date: '2021-05-10') 135 | # ``` 136 | def self.download_exception_withdraw_file(params) 137 | bill_type = params.delete(:bill_type) 138 | path = "/v3/merchant/fund/withdraw/bill-type/#{bill_type}" 139 | 140 | method = 'GET' 141 | 142 | query = build_query(params) 143 | url = "#{path}?#{query}" 144 | 145 | make_request( 146 | method: method, 147 | path: url, 148 | extra_headers: { 149 | 'Content-Type' => 'application/x-www-form-urlencoded' 150 | } 151 | ) 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /spec/wechat-pay/sign_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'WechatPay::Sign' do 6 | before do 7 | @wechat_platform_key = OpenSSL::PKey::RSA.new(File.read('spec/fixtures/random_platform_key.pem')) 8 | end 9 | 10 | it 'generate miniprogramming payment params from prepay id and appid' do 11 | timestamp = Time.at(1_600_000) 12 | hex = 'hhhhhhhhh' 13 | appid = 'wx18802234' 14 | prepay_id = 'prepay_id' 15 | 16 | expect(Time).to receive(:now).with(any_args).and_return(timestamp) 17 | expect(SecureRandom).to receive(:hex).with(any_args).and_return(hex) 18 | 19 | params = WechatPay::Sign.generate_app_payment_params_from_prepay_id_and_appid(appid, prepay_id) 20 | result = { 'partnerId' => WechatPay.mch_id, 'appId' => appid, 'packageValue' => 'Sign=WXPay', 'timeStamp' => '1600000', 'nonceStr' => 'hhhhhhhhh', 'prepayId' => prepay_id, 'sign' => 'iGxYj/P4E4PG8bnCmLWCXn4sRxumpPY2d7Z6LuRvHPgTh7vB61d/fS9jGKBLooqxRUpS0of6egMVGmbNKa1tYj028P5AGIdfkfdIQcYpL9gHTvDfW0abZ1GK+79HtlcHXkuwJS6E8N1Q/ZliG9csCzB5AwzW40PGilDWzMzfkycvQkcuR0jhLx+RWMWFU1SupBqogT7KwX0En1956zKM0lRWMpclvH8BWqGgMxuFOy0pUCjYGSyUtlaP42p8vL+WLyw/JT70QQsmRCnCYAi8l7uSq3YKRc7JTA7WBmkrhU8lvNbyCz7YmIOspkWMgyBPuX5qjcl0EGhHZy3EDq6/Dg==' } 21 | expect(params).to eq(result) 22 | end 23 | 24 | it 'generate miniprogramming payment params from prepay id and appid' do 25 | timestamp = Time.at(1_600_000) 26 | hex = 'hhhhhhhhh' 27 | appid = 'wx18802234' 28 | prepay_id = 'prepay_id' 29 | 30 | expect(Time).to receive(:now).with(any_args).and_return(timestamp) 31 | expect(SecureRandom).to receive(:hex).with(any_args).and_return(hex) 32 | 33 | params = WechatPay::Sign.generate_payment_params_from_prepay_id_and_appid(appid, prepay_id) 34 | result = { 'timeStamp' => '1600000', 'nonceStr' => 'hhhhhhhhh', 'package' => 'prepay_id=prepay_id', 35 | 'paySign' => 'aTNV10+9mzmONvPuL6lSW+LRvNa1Z3LVAVTAKjYyJ7LmgHjIZ8piKNBBOCRx5l+71QBZoFBDqEjAfi4rHdSIaO4vGfAXxNqsZNjueNlE1yQEgKHDarYWV/uLuxzNCgG1vwz6cTzP8TvCXsihrL43duNxNNgR8ERxGs5/IOXKANc59vlYW1eeOjc98Q+0J1OQPlTIMDzFXqjf9i9dkGdJfFG5Z+bDceeDcGhZEw8+T6kJSvWRZxRzJ1C7NKR2ITkX7Yg9joxz6oCwhfcsrnOkOI8gwdK/L/OKjP7TQzntQKWjfYAcmdOUTuXhLpymcunUkKUd0gXv3SNwFdQk0vzmMg==', 'signType' => 'RSA' } 36 | expect(params).to eq(result) 37 | end 38 | 39 | it 'build authorization header' do 40 | timestamp = Time.at(1_600_000) 41 | hex = 'hhhhhhhhh' 42 | 43 | expect(Time).to receive(:now).with(any_args).and_return(timestamp) 44 | expect(SecureRandom).to receive(:hex).with(any_args).and_return(hex) 45 | 46 | authorization_header = WechatPay::Sign.build_authorization_header('GET', '/v3/api/wechat_pay', 47 | { name: 'Ruby' }.to_json) 48 | expected_string = format( 49 | 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",serial_no="%s",signature="b61tFFRwxJgzO+QeTHiRxxiOpniZCK2cwUtnBEE7QVe06k3tNjfIaXtrOOSc3Gr84mx9xlQePM7s2cf1B3ixYKnvcJiTNnUsQwU9omyyUSjO1YIdwQTrra1r6+VUmM7pAq1eowa/WQhvAP2QkG2J4ienMwNtVuHb4Tw7X1R7LSh2/DEl+LmYCtEd7Acc4AMFyLE/rqdN19fdO+ZSTpVy0rTDMSsgpCACP8Xi7lQFyej9Gb72XYq0oHelWpCSyIRoWm7214ck76ytcgPIe15jOpLYO+L2cYf5VSMPAJ9neX45udpBuYXJWC6NchHko/HNN473zlNqOb6gCqwugzkvNg==",timestamp="%s"', timestamp: timestamp.to_i, mchid: WechatPay.mch_id, hex: hex, serial_no: WechatPay.apiclient_serial_no 50 | ) 51 | expect(authorization_header).to eq(expected_string) 52 | end 53 | 54 | it 'sign string with sha256' do 55 | test_string = 'Hello Ruby' 56 | signature = WechatPay::Sign.sign_string(test_string) 57 | apiclient_public_key = WechatPay.apiclient_cert.public_key 58 | decoded_signature = Base64.strict_decode64(signature) 59 | expect(apiclient_public_key.verify('SHA256', decoded_signature, test_string)).to eq(true) 60 | end 61 | 62 | it 'sign important info' do 63 | important_text = 'Ruby' 64 | signature = WechatPay::Sign.sign_important_info(important_text) 65 | result = @wechat_platform_key.private_decrypt(Base64.strict_decode64(signature), 66 | OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) 67 | expect(result).to eq(important_text) 68 | end 69 | 70 | it 'decrypt the encrypt params from wechat' do 71 | data = 'Very, very confidential data' 72 | nonce = '46e4d8f11f62' # 必须是12个字节 73 | associated_data = 'transaction' 74 | cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt 75 | cipher.key = WechatPay.mch_key 76 | cipher.iv = nonce 77 | cipher.auth_data = associated_data 78 | encrypted = cipher.update(data) + cipher.final 79 | tag = cipher.auth_tag # produces 16 bytes tag by default 80 | ciphertext = Base64.strict_encode64(encrypted + tag) 81 | 82 | result = WechatPay::Sign.decrypt_the_encrypt_params( 83 | associated_data: associated_data, 84 | nonce: nonce, 85 | ciphertext: ciphertext 86 | ) 87 | expect(data).to eq(result) 88 | end 89 | 90 | it 'if notifications from wechat' do 91 | timestamp = 1_600_000 92 | noncestr = 'hhhhhhh' 93 | body = { name: 'Ruby', age: '26' }.to_json 94 | callback_string = WechatPay::Sign.send(:build_callback_string, timestamp, noncestr, body) 95 | signature = Base64.strict_encode64(@wechat_platform_key.sign('SHA256', callback_string)) 96 | 97 | expect(WechatPay::Sign.notification_from_wechat?(timestamp, noncestr, body, signature)).to eq(true) 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/wechat-pay/ecommerce/applyment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WechatPay 4 | # 商户进件相关 5 | module Ecommerce 6 | # 二级商户进件 7 | # 8 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter11_1_1.shtml 9 | # 10 | # Example: 11 | # 12 | # ``` ruby 13 | # params = { 14 | # "organization_type": "2", 15 | # "business_license_info": { 16 | # "business_license_copy": "47ZC6GC-vnrbEg05InE4d2I6_H7I4", 17 | # "business_license_number": "123456789012345678", 18 | # "merchant_name": "腾讯科技有限公司", 19 | # "legal_person": "张三" 20 | # }, 21 | # "organization_cert_info": { 22 | # "organization_copy": "47ZC6GC-vnrbEny__Ie_An5-tCpqxuGprrKhpVBDIUv0OF4wFNIO4kqg05InE4d2I6_H7I4 ", 23 | # "organization_time": "[\"2014-01-01\",\"长期\"]", 24 | # "organization_number": "12345679-A" 25 | # }, 26 | # "id_card_info": { 27 | # "id_card_copy": "jTpGmxUX3FBWVQ5NJTZvlKC-ehEuo0BJqRTvDujqhThn4ReFxikqJ5YW6zFQ", 28 | # "id_card_national": "47ZC6GC-vnrbEny__Ie_AnGprrKhpVBDIUv0OF4wFNIO4kqg05InE4d2I6_H7I4", 29 | # "id_card_name": "pVd1HJ6z7UtC + xriudjD5AqhZ9evAM + Jv1z0NVa8MRtelw / wDa4SzfeespQO / 0 kjiwfqdfg =", 30 | # "id_card_number": "UZFETyabYFFlgvGh6R4vTzDRgzvA2HtP5VHahNhSUqhR9iuGTunRPRVFg ==", 31 | # "id_card_valid_time": "2026-06-06" 32 | # }, 33 | # "need_account_info": true, 34 | # "account_info": { 35 | # "bank_account_type": "74", 36 | # "account_name": "fTA4TXc8yMOwFCYeGPktOOSbOBei3KAmUWHGxCQo2hfaC7xumTqXR7 / NyRHpFKXURQFcmmw ==", 37 | # "account_bank": "工商银行", 38 | # "bank_address_code": "110000", 39 | # "bank_branch_id": "402713354941", 40 | # "bank_name": "施秉县农村信用合作联社城关信用社", 41 | # "account_number": "d+xT+MQCvrLHUVD5Fnx30mr4L8sPndjXTd75kPkyjqnoMRrEEaYQE8ZRGYoeorwC" 42 | # }, 43 | # "contact_info": { 44 | # "contact_type": "65", 45 | # "contact_name": "pVd1HJ6zyvPedzGaV+Xy7UDa4SzfeespQO / 0 kjiwfqdfg ==", 46 | # "contact_id_card_number": "UZFETyabYFFlgvGh6R4vTzDEOiZZ4ka9+5RgzvA2rJx+NztYUbN209rqR9iuGTunRPRVFg ==", 47 | # "mobile_phone": "Uy5Hb0c5Se/orEbrWze/ROHu9EPAs/CigDlJ2fnyzC1ppJNBrqcBszhYQUlu5zn6o2uZpBhAsQwd3QAjw==", 48 | # "contact_email": "Uy5Hb0c5Se/orEbrWze/ROHu9EPAs/CigDlJ2fnyzC1ppJNaLZExOEzmUn6o2uZpBhAsQwd3QAjw==" 49 | # }, 50 | # "sales_scene_info": { 51 | # "store_name": "爱烧烤", 52 | # "store_url": "http://www.qq.com", 53 | # "store_qr_code": "jTpGmxUX3FBWVQ5NJTZvlKX_gdU4cRz7z5NxpnFuAujqhThn4ReFxikqJ5YW6zFQ" 54 | # }, 55 | # "merchant_shortname": "爱烧烤", 56 | # "out_request_no": "APPLYMENT_00000000001", 57 | # "qualifications": "[\"jTpGmxUX3FBWVQ5NJInE4d2I6_H7I4\"]", 58 | # "business_addition_pics": "[\"jTpGmg05InE4d2I6_H7I4\"]", 59 | # "business_addition_desc": "特殊情况,说明原因" 60 | # } 61 | # ``` 62 | # 63 | # ``` ruby 64 | # WechatPay::Ecommerce.applyment(params) 65 | # ``` 66 | # 67 | def self.applyment(payload) 68 | url = '/v3/ecommerce/applyments/' 69 | method = 'POST' 70 | 71 | payload_json = payload.to_json 72 | 73 | make_request( 74 | method: method, 75 | path: url, 76 | for_sign: payload_json, 77 | payload: payload_json, 78 | extra_headers: { 79 | 'Wechatpay-Serial' => WechatPay.platform_serial_no 80 | } 81 | ) 82 | end 83 | 84 | QUERY_APPLYMENT_FIELDS = %i[applyment_id out_request_no].freeze # :nodoc: 85 | # 86 | # 通过商户的申请单或微信的申请单号查询 87 | # 88 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter11_1_2.shtml 89 | # 90 | # Example: 91 | # 92 | # ``` ruby 93 | # WechatPay::Ecommerce.query_applyment(out_request_no: 'APPLYMENT_00000000005') # by out_request_no 94 | # WechatPay::Ecommerce.query_applyment(applyment_id: '200040444455566667') # by_applyment_id 95 | # ``` 96 | # 97 | def self.query_applyment(params) 98 | if params[:applyment_id] 99 | applyment_id = params.delete(:applyment_id) 100 | url = "/v3/ecommerce/applyments/#{applyment_id}" 101 | else 102 | out_request_no = params.delete(:out_request_no) 103 | url = "/v3/ecommerce/applyments/out-request-no/#{out_request_no}" 104 | end 105 | method = 'GET' 106 | 107 | make_request( 108 | method: method, 109 | path: url, 110 | extra_headers: { 111 | 'Content-Type' => 'application/x-www-form-urlencoded' 112 | } 113 | ) 114 | end 115 | 116 | # 证书获取 117 | # 118 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_1_3.shtml 119 | # 120 | # Example: 121 | # 122 | # ``` ruby 123 | # WechatPay::Ecommerce.certificates 124 | # ``` 125 | # 126 | def self.certificates 127 | url = '/v3/certificates' 128 | method = 'GET' 129 | 130 | make_request( 131 | method: method, 132 | path: url, 133 | extra_headers: { 134 | 'Content-Type' => 'application/x-www-form-urlencoded' 135 | } 136 | ) 137 | end 138 | 139 | INVOKE_QUERY_SETTLEMENT_FIELDS = [:sub_mchid].freeze # :nodoc: 140 | # 141 | # 查询结算账户 142 | # 143 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_1_5.shtml 144 | # 145 | # Example: 146 | # 147 | # ``` ruby 148 | # WechatPay::Ecommerce.query_settlement(sub_mchid: '16000000') 149 | # ``` 150 | # 151 | def self.query_settlement(params) 152 | sub_mchid = params.delete(:sub_mchid) 153 | url = "/v3/apply4sub/sub_merchants/#{sub_mchid}/settlement" 154 | method = 'GET' 155 | 156 | make_request( 157 | method: method, 158 | path: url, 159 | extra_headers: { 160 | 'Content-Type' => 'application/x-www-form-urlencoded' 161 | } 162 | ) 163 | end 164 | 165 | INVOKE_MODIFY_SETTLEMENT_FIELDS = %i[sub_mchid account_type account_bank bank_address_code account_number].freeze # :nodoc: 166 | # 167 | # 修改结算账号 168 | # 169 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_1_4.shtml 170 | # 171 | # Example: 172 | # 173 | # ``` ruby 174 | # WechatPay::Ecommerce.modify_settlement(sub_mchid: '15000000', account_type: 'ACCOUNT_TYPE_PRIVATE', account_bank: '工商银行', bank_address_code: '110000', account_number: WechatPay::Sign.sign_important_info('755555555')) 175 | # ``` 176 | # 177 | def self.modify_settlement(params) 178 | sub_mchid = params.delete(:sub_mchid) 179 | url = "/v3/apply4sub/sub_merchants/#{sub_mchid}/modify-settlement" 180 | method = 'POST' 181 | 182 | payload_json = params.to_json 183 | 184 | make_request( 185 | method: method, 186 | path: url, 187 | for_sign: payload_json, 188 | payload: payload_json, 189 | extra_headers: { 190 | 'Wechatpay-Serial' => WechatPay.platform_serial_no 191 | } 192 | ) 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /lib/wechat-pay/ecommerce/profitsharing.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WechatPay 4 | # 分账相关 5 | module Ecommerce 6 | class << self 7 | REQUEST_PROFITSHARING_FIELDS = %i[out_trade_no transaction_id sub_mchid out_order_no receivers finish].freeze # :nodoc: 8 | # 9 | # 分账请求 10 | # 11 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_4_1.shtml 12 | # 13 | # Example: 14 | # 15 | # ``` ruby 16 | # params = {"out_trade_no"=>"P202104306585", "transaction_id"=>"4323400972202104301286330188", "sub_mchid"=>"160000", "out_order_no"=>"N202104307987", "finish"=>true, "receivers"=>[{"type"=>"MERCHANT_ID", "receiver_account"=>"1607189890", "amount"=>1, "description"=>"平台抽成", "receiver_name"=>"CXOO5SF5sylMhSWjUBHQ6dBN0BTdrGExiziO8OEnJEG/nAa7gw6JTbsFQVhUbXD2er07Gcvt7qsLg7wYEe6iqNKbHHRWvChVVKWcKSyvfMOcRa95lxUkVn2+YdMmQ/Rt2h+xN7HMFMVPh9Py2c3sxnv1hZSraTEBWp577NOVwfSKiDTOAnbLtVtLbJndZ2N/bRXzW/gpbQV6TnnsrKPJ+NQ64kCedaYoO0XvEK1JavJju4kUAw/TnJ78jBMwj0gx2kfrsAgtwGrIGhrqhGcGHwwwPPDk5lS/iVaKpSdMvxOHN/9mrAqgqmvBg9uHRKE4sUqkZWuaiAFvYF9/5sLgjQ=="}]} 17 | # WechatPay::Ecommerce.request_profitsharing(params) 18 | # ``` 19 | def request_profitsharing(params) 20 | url = '/v3/ecommerce/profitsharing/orders' 21 | method = 'POST' 22 | params = { 23 | appid: WechatPay.app_id 24 | }.merge(params) 25 | 26 | payload_json = params.to_json 27 | 28 | make_request( 29 | method: method, 30 | path: url, 31 | for_sign: payload_json, 32 | payload: payload_json, 33 | extra_headers: { 34 | 'Wechatpay-Serial' => WechatPay.platform_serial_no 35 | } 36 | ) 37 | end 38 | 39 | QUERY_PROFITSHARING_FIELDS = %i[out_order_no transaction_id sub_mchid].freeze # :nodoc: 40 | # 41 | # 分账结果查询 42 | # 43 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_4_2.shtml 44 | # 45 | # Example: 46 | # 47 | # ``` ruby 48 | # WechatPay::Ecommerce.query_profitsharing(out_order_no: 'N202104288345', sub_mchid: '16000000', transaction_id: '4200001048202104280183691118') 49 | # ``` 50 | # 51 | def query_profitsharing(params) 52 | method = 'GET' 53 | query = build_query(params) 54 | path = '/v3/ecommerce/profitsharing/orders' 55 | url = "#{path}?#{query}" 56 | 57 | make_request( 58 | path: url, 59 | method: method, 60 | extra_headers: { 61 | 'Content-Type' => 'application/x-www-form-urlencoded' 62 | } 63 | ) 64 | end 65 | 66 | RETURN_PROFITSHARING_FIELDS = %i[sub_mchid order_id out_order_no out_return_no return_mchid amount description].freeze # :nodoc: 67 | # 68 | # 请求分账回退 69 | # 70 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_4_3.shtml 71 | # 72 | # ``` ruby 73 | # WechatPay::Ecommerce.return_profitsharing(out_order_no: 'P202104306585', sub_mchid: '16000000', out_return_no: 'R20210430223', return_mchid: '180000', amount: 1, description: '分账回退') 74 | # WechatPay::Ecommerce.return_profitsharing(order_id: '3008450740201411110007820472', sub_mchid: '16000000', out_return_no: 'R20210430223', return_mchid: '180000', amount: 1, description: '分账回退') 75 | # ``` 76 | def return_profitsharing(params) 77 | url = '/v3/ecommerce/profitsharing/returnorders' 78 | method = 'POST' 79 | 80 | payload_json = params.to_json 81 | 82 | make_request( 83 | method: method, 84 | path: url, 85 | for_sign: payload_json, 86 | payload: payload_json 87 | ) 88 | end 89 | 90 | QUERY_RETURN_PROFITSHARING_FIELDS = %i[sub_mchid order_id out_order_no out_return_no].freeze # :nodoc: 91 | # 92 | # 分账回退结果查询 93 | # 94 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_4_4.shtml 95 | # 96 | # Example: 97 | # 98 | # ``` ruby 99 | # WechatPay::Ecommerce.query_return_profitsharing(sub_mchid: '1608747309', out_order_no: 'P202104306585', out_return_no: 'R202105023455') 100 | # WechatPay::Ecommerce.query_return_profitsharing(sub_mchid: '1608747309', order_id: '3008450740201411110007820472', out_return_no: 'R202105023455') 101 | # ``` 102 | def query_return_profitsharing(params) 103 | method = 'GET' 104 | query = build_query(params) 105 | path = '/v3/ecommerce/profitsharing/returnorders' 106 | url = "#{path}?#{query}" 107 | 108 | make_request( 109 | path: url, 110 | method: method, 111 | extra_headers: { 112 | 'Content-Type' => 'application/x-www-form-urlencoded' 113 | } 114 | ) 115 | end 116 | 117 | FINISH_PROFITSHARING_FIELDS = %i[transaction_id sub_mchid out_order_no description].freeze # :nodoc: 118 | # 119 | # 完结分账 120 | # 121 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_4_5.shtml 122 | # 123 | # Example: 124 | # 125 | # ``` ruby 126 | # WechatPay::Ecommerce.finish_profitsharing(sub_mchid: '160000', out_order_no: 'P202104303106', transaction_id: '4323400972202104305131070133', description: '直接打款到二级商户不分账').bod 127 | # ``` 128 | # 129 | def finish_profitsharing(params) 130 | url = '/v3/ecommerce/profitsharing/finish-order' 131 | method = 'POST' 132 | 133 | payload_json = params.to_json 134 | 135 | make_request( 136 | method: method, 137 | path: url, 138 | for_sign: payload_json, 139 | payload: payload_json 140 | ) 141 | end 142 | 143 | QUERY_PROFITSHARING_AMOUNT_FIELDS = %i[transaction_id].freeze # :nodoc: 144 | # 145 | # 查询订单剩余待分金额 146 | # 147 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_4_9.shtml 148 | # 149 | # Example: 150 | # 151 | # ``` ruby 152 | # WechatPay::Ecommerce.query_profitsharing_amount({ transaction_id: '4323400972202104301286330188' }) 153 | # ``` 154 | # 155 | def query_profitsharing_amount(params) 156 | method = 'GET' 157 | transaction_id = params.delete(:transaction_id) 158 | url = "/v3/ecommerce/profitsharing/orders/#{transaction_id}/amounts" 159 | 160 | make_request( 161 | path: url, 162 | method: method, 163 | extra_headers: { 164 | 'Content-Type' => 'application/x-www-form-urlencoded' 165 | } 166 | ) 167 | end 168 | 169 | ADD_PROFITSHARING_RECEIVERS_FIELDS = %i[type account name relation_type].freeze # :nodoc: 170 | # 171 | # 添加分账接收方 172 | # 173 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_4_7.shtml 174 | # 175 | # Example: 176 | # 177 | # ``` ruby 178 | # WechatPay::Ecommerce.add_profitsharing_receivers(type: 'PERSONAL_OPENID', account: 'oly6s5cLmmVzzr8iPyI6mJj7qG2s', name: 'Lan', relation_type: 'DISTRIBUTOR').body 179 | # ``` 180 | # 181 | def add_profitsharing_receivers(params) 182 | url = '/v3/ecommerce/profitsharing/receivers/add' 183 | method = 'POST' 184 | 185 | params = { 186 | appid: WechatPay.app_id 187 | }.merge(params) 188 | 189 | payload_json = params.to_json 190 | 191 | make_request( 192 | method: method, 193 | path: url, 194 | for_sign: payload_json, 195 | payload: payload_json 196 | ) 197 | end 198 | 199 | DELETE_PROFITSHARING_RECEIVERS_FIELDS = %i[type account].freeze # :nodoc: 200 | # 201 | # 删除分账接收方 202 | # 203 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_4_8.shtml 204 | # 205 | # Example: 206 | # 207 | # ``` ruby 208 | # WechatPay::Ecommerce.remove_profitsharing_receivers(type: 'PERSONAL_OPENID', account: 'oly6s5cLmmVzzr8iPyI6mJj7qG2s', name: 'Lan', relation_type: 'DISTRIBUTOR').body 209 | # ``` 210 | # 211 | def delete_profitsharing_receivers(params) 212 | url = '/v3/ecommerce/profitsharing/receivers/delete' 213 | method = 'POST' 214 | 215 | params = { 216 | appid: WechatPay.app_id 217 | }.merge(params) 218 | 219 | payload_json = params.to_json 220 | 221 | make_request( 222 | method: method, 223 | path: url, 224 | for_sign: payload_json, 225 | payload: payload_json 226 | ) 227 | end 228 | end 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /lib/wechat-pay/sign.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'openssl' 4 | require 'base64' 5 | require 'securerandom' 6 | require 'active_support/core_ext/hash' 7 | 8 | module WechatPay 9 | # # 微信签名相关的封装 10 | # 文档: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml 11 | # 12 | # PS: 提供了常用的帮助方法,方便您的开发 13 | # 14 | module Sign 15 | class << self 16 | # Generate payment params with appid and prepay_id for invoking the wechat pay in app 17 | # 18 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_2_7.shtml 19 | # 20 | # Take app for example 21 | # 22 | # ``` ruby 23 | # appid = 'appid for mobile' 24 | # 25 | # params = { 26 | # sp_appid: 'Your appid', 27 | # sp_mchid: 'Your mchid', 28 | # description: 'pay', 29 | # out_trade_no: 'Order Number', 30 | # amount: { 31 | # total: 10 32 | # }, 33 | # sub_mchid: 'Your sub mchid', 34 | # notify_url: 'the url' 35 | # } 36 | # result = WechatPay::Ecommerce.invoke_transactions_in_app(params).body 37 | # # => { prepay_id => 'wx201410272009395522657a690389285100' } 38 | # prepay_id = result['prepay_id'] 39 | # WechatPay::Sign.generate_app_payment_params_from_prepay_id_and_appid(appid, prepay_id) 40 | # # => params for invoking the wechat pay in app 41 | # ``` 42 | 43 | def generate_app_payment_params_from_prepay_id_and_appid(appid, prepay_id) 44 | timestamp = Time.now.to_i.to_s 45 | noncestr = SecureRandom.hex 46 | string = build_app_paysign_string(appid, timestamp, noncestr, prepay_id) 47 | 48 | { 49 | appId: appid, 50 | partnerId: WechatPay.mch_id, 51 | timeStamp: timestamp, 52 | nonceStr: noncestr, 53 | prepayId: prepay_id, 54 | packageValue: 'Sign=WXPay', 55 | sign: sign_string(string) 56 | }.stringify_keys 57 | end 58 | 59 | # Generate payment params with appid and prepay_id for invoking the wechat pay in miniprogram 60 | # 61 | # 62 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_2_8.shtml 63 | # 64 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_2_9.shtml 65 | # 66 | # Take app for example 67 | # 68 | # ``` ruby 69 | # appid = 'appid for mobile' 70 | # 71 | # params = { 72 | # sp_appid: 'Your appid', 73 | # sp_mchid: 'Your mchid', 74 | # description: 'pay', 75 | # out_trade_no: 'Order Number', 76 | # payer: { 77 | # sp_openid: 'wechat open id' 78 | # }, 79 | # amount: { 80 | # total: 10 81 | # }, 82 | # sub_mchid: 'Your sub mchid', 83 | # notify_url: 'the url' 84 | # } 85 | # result = WechatPay::Ecommerce.invoke_transactions_in_miniprogram(params).body 86 | # # => { prepay_id => 'wx201410272009395522657a690389285100' } 87 | # prepay_id = result['prepay_id'] 88 | # WechatPay::Sign.generate_payment_params_from_prepay_id_and_appid(appid, prepay_id) 89 | # # => params for invoking the wechat pay in miniprogram 90 | # ``` 91 | def generate_payment_params_from_prepay_id_and_appid(appid, prepay_id) 92 | timestamp = Time.now.to_i.to_s 93 | noncestr = SecureRandom.hex 94 | string = build_paysign_string(appid, timestamp, noncestr, prepay_id) 95 | 96 | { 97 | timeStamp: timestamp, 98 | nonceStr: noncestr, 99 | package: "prepay_id=#{prepay_id}", 100 | paySign: sign_string(string), 101 | signType: 'RSA' 102 | }.stringify_keys 103 | end 104 | 105 | # For checkingi if the requests from wechat platform 106 | # 107 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_2_11.shtml 108 | # 109 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml 110 | # 111 | # Usage: 112 | # 113 | # ``` ruby 114 | # def pay_action 115 | # timestamp = request.headers['Wechatpay-Timestamp'] 116 | # noncestr = request.headers['Wechatpay-Nonce'] 117 | # signature = request.headers['Wechatpay-Signature'] 118 | # body = JSON.parse(request.body.read) 119 | # raise Exceptions::InvalidAction, '非法请求,请求并非来自微信' unless WechatV3.notification_from_wechat?(timestamp, noncestr, body.to_json, signature) 120 | # # .... 121 | # end 122 | # ``` 123 | def notification_from_wechat?(timestamp, noncestr, json_body, signature) 124 | string = build_callback_string(timestamp, noncestr, json_body) 125 | decoded_signature = Base64.strict_decode64(signature) 126 | WechatPay.platform_cert.public_key.verify('SHA256', decoded_signature, string) 127 | end 128 | 129 | # For signing the sensitive information 130 | # 131 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_3.shtml 132 | # 133 | # Usage: 134 | # 135 | # ``` ruby 136 | # string = 'Ruby' 137 | # WechatPay::Sign.sign_important_info(string) 138 | # ``` 139 | # 140 | # ``` ruby 141 | # # result 142 | # "K0MK7g3laREAQ4HIlpIndVmFdz4IyxxiVp42hXFx2CzWRB1fn85ANBxnQXESq91vJ1P9mCt94cHZDoshlEOJRkE1KvcxpBCnG3ghIqiSsLKdLZ3ytO94GBDzCt8nsq+vJKXJbK2XuL9p5h0KYGKZyjt2ydU9Ig6daWTpZH8lAKIsLzPTsaUtScuw/v3M/7t8/4py8N0MOLKbDBDnR5Q+MRHbEWI9nCA3HTAWsSerIIgE7igWnzybxsUzhkV8m49P/Shr2zh6yJAlEnyPLFmQG7GuUaYwDTSLKOWzzPYwxMcucWQha2krC9OlwnZJe6ZWUAI3s4ej4kFRfheOYywRoQ==" 143 | # ``` 144 | def sign_important_info(string) 145 | platform_public_key = WechatPay.platform_cert.public_key 146 | Base64.strict_encode64(platform_public_key.public_encrypt(string, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)) 147 | end 148 | 149 | # For Decrypting the encrypt params from wechat platform 150 | # 151 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_2.shtml 152 | # 153 | # Usage: 154 | # 155 | # ``` ruby 156 | # def pay_action 157 | # # check if the request from wechat 158 | # associated_data = body['resource']['associated_data'] 159 | # nonce = body['resource']['nonce'] 160 | # ciphertext = body['resource']['ciphertext'] 161 | # res = WechatPay::Sign.decrypt_the_encrypt_params( 162 | # associated_data: associated_data, 163 | # nonce: nonce, 164 | # ciphertext: ciphertext 165 | # ) 166 | # result = JSON.parse(res) # Get the real params 167 | # end 168 | # ``` 169 | # 170 | def decrypt_the_encrypt_params(associated_data:, nonce:, ciphertext:) 171 | # https://contest-server.cs.uchicago.edu/ref/ruby_2_3_1_stdlib/libdoc/openssl/rdoc/OpenSSL/Cipher.html 172 | tag_length = 16 173 | decipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt 174 | decipher.key = WechatPay.mch_key 175 | decipher.iv = nonce 176 | signature = Base64.strict_decode64(ciphertext) 177 | length = signature.length 178 | real_signature = signature.slice(0, length - tag_length) 179 | tag = signature.slice(length - tag_length, length) 180 | decipher.auth_tag = tag 181 | decipher.auth_data = associated_data 182 | decipher.update(real_signature) 183 | end 184 | 185 | # Build authorization header for request 186 | # 187 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_0.shtml 188 | # 189 | # Usage: 190 | # 191 | # ``` ruby 192 | # method = 'GET' 193 | # url = '/v3/certificates' 194 | # json_body = '' 195 | # WechatPay::sign.build_authorization_header(method, url, json_body) 196 | # ``` 197 | # 198 | # ``` ruby 199 | # # Result 200 | # "WECHATPAY2-SHA256-RSA2048 mchid=\"16000000\",nonce_str=\"42ac357637f9331794e0c6fb3b3de048\",serial_no=\"0254A801C0\",signature=\"WBJaWlVFur5OGQ/E0ZKIlSDhR8WTNrkW2oCECF3Udrh8BVlnfYf5N5ROeOt9PBxdwD0+ufFQANZKugmXDNat+sFRY2DrIzdP3qYvFIzaYjp6QEtB0UPzvTgcLDULGbwCSTNDxvKRDi07OXPFSmVfmA5SbpbfumgjYOfzt1wcl9Eh+/op/gAB3N010Iu1w4OggR78hxQvPb9GIscuKHjaUWqqwf6v+p3/b0tiSO/SekJa3bMKPhJ2wJj8utBHQtbGO+iUQj1n90naL25MNJUM2XYocv4MasxZZgZnV3v1dtRvFkVo0ApqFyDoiRndr1Q/jPh+wmsb80LuhZ1S4eNfew==\",timestamp=\"1620571488\"" 201 | # ``` 202 | def build_authorization_header(method, url, json_body) 203 | timestamp = Time.now.to_i 204 | nonce_str = SecureRandom.hex 205 | string = build_string(method, url, timestamp, nonce_str, json_body) 206 | signature = sign_string(string) 207 | 208 | params = { 209 | mchid: WechatPay.mch_id, 210 | nonce_str: nonce_str, 211 | serial_no: WechatPay.apiclient_serial_no, 212 | signature: signature, 213 | timestamp: timestamp 214 | } 215 | 216 | params_string = params.stringify_keys.map { |key, value| "#{key}=\"#{value}\"" }.join(',') 217 | 218 | "WECHATPAY2-SHA256-RSA2048 #{params_string}" 219 | end 220 | 221 | def sign_string(string) 222 | result = WechatPay.apiclient_key.sign('SHA256', string) # 商户私钥的SHA256-RSA2048签名 223 | Base64.strict_encode64(result) # Base64处理 224 | end 225 | 226 | private 227 | 228 | def build_string(method, url, timestamp, noncestr, body) 229 | "#{method}\n#{url}\n#{timestamp}\n#{noncestr}\n#{body}\n" 230 | end 231 | 232 | def build_callback_string(timestamp, noncestr, body) 233 | "#{timestamp}\n#{noncestr}\n#{body}\n" 234 | end 235 | 236 | def build_paysign_string(appid, timestamp, noncestr, prepayid) 237 | "#{appid}\n#{timestamp}\n#{noncestr}\nprepay_id=#{prepayid}\n" 238 | end 239 | 240 | def build_app_paysign_string(appid, timestamp, noncestr, prepayid) 241 | "#{appid}\n#{timestamp}\n#{noncestr}\n#{prepayid}\n" 242 | end 243 | end 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /lib/wechat-pay/direct.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'wechat-pay/helper' 5 | 6 | module WechatPay 7 | # # 直连商户相关接口封装(常用的已有,待完善) 8 | # 文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml 9 | module Direct 10 | include WechatPayHelper 11 | 12 | # @private 13 | # @!macro [attach] define_transaction_method 14 | # 直连$1下单 15 | # 16 | # Document: $3 17 | # 18 | # Example: 19 | # 20 | # ``` ruby 21 | # params = { 22 | # appid: 'Your Open id', 23 | # mchid: 'Your Mch id'', 24 | # description: '回流', 25 | # out_trade_no: 'Checking', 26 | # payer: { 27 | # openid: 'oly6s5c' 28 | # }, 29 | # amount: { 30 | # total: 1 31 | # }, 32 | # notify_url: ENV['NOTIFICATION_URL'] 33 | # } 34 | # 35 | # WechatPay::Direct.invoke_transactions_in_$1(params) 36 | # ``` 37 | # @!method invoke_transactions_in_$1 38 | # @!scope class 39 | def self.define_transaction_method(key, value, _document) 40 | const_set("INVOKE_TRANSACTIONS_IN_#{key.upcase}_FIELDS", 41 | %i[description out_trade out_trade_no payer amount notify_url].freeze) 42 | define_singleton_method "invoke_transactions_in_#{key}" do |params| 43 | direct_transactions_method_by_suffix(value, params) 44 | end 45 | end 46 | 47 | define_transaction_method('js', 'jsapi', 'https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml') 48 | define_transaction_method('miniprogram', 'jsapi', 'https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml') 49 | define_transaction_method('app', 'app', 'https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_1.shtml') 50 | define_transaction_method('h5', 'h5', 'https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml') 51 | define_transaction_method('native', 'native', 'https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml') 52 | 53 | # @private 54 | # @!macro [attach] define_combine_transaction_method 55 | # 直连合单$1下单 56 | # 57 | # Document: $3 58 | # 59 | # ``` ruby 60 | # params = { 61 | # combine_out_trade_no: 'combine_out_trade_no', 62 | # combine_payer_info: { 63 | # openid: 'client open id' 64 | # }, 65 | # sub_orders: [ 66 | # { 67 | # mchid: 'mchid', 68 | # sub_mchid: 'sub mchid', 69 | # attach: 'attach', 70 | # amount: { 71 | # total_amount: 100, 72 | # currency: 'CNY' 73 | # }, 74 | # out_trade_no: 'out_trade_no', 75 | # description: 'description' 76 | # } 77 | # ], 78 | # notify_url: 'the_url' 79 | # } 80 | # 81 | # WechatPay::Direct.invoke_combine_transactions_in_$1(params) 82 | # ``` 83 | # @!method invoke_combine_transactions_in_$1 84 | # @!scope class 85 | def self.define_combine_transaction_method(key, _value, _document) 86 | const_set("INVOKE_COMBINE_TRANSACTIONS_IN_#{key.upcase}_FIELDS", 87 | %i[combine_out_trade_no scene_info sub_orders notify_url].freeze) 88 | define_singleton_method("invoke_combine_transactions_in_#{key}") do |params| 89 | WechatPay::Ecommerce.send("invoke_combine_transactions_in_#{key}", params) 90 | end 91 | end 92 | 93 | define_combine_transaction_method('app', 'app', 'https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_1.shtml') 94 | define_combine_transaction_method('js', 'jsapi', 'https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_3.shtml') 95 | define_combine_transaction_method('h5', 'h5', 'https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_2.shtml') 96 | define_combine_transaction_method('miniprogram', 'jsapi', 'https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_4.shtml') 97 | define_combine_transaction_method('native', 'native', 'https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_5.shtml') 98 | 99 | QUERY_COMBINE_ORDER_FIELDS = %i[combine_out_trade_no].freeze # :nodoc: 100 | # 101 | # 合单查询 102 | # 103 | # TODO: 与电商平台相同,稍后重构 104 | # 105 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_3_11.shtml 106 | # 107 | # ``` ruby 108 | # WechatPay::Direct.query_order(combine_out_trade_no: 'C202104302474') 109 | # ``` 110 | # 111 | def self.query_combine_order(params) 112 | combine_out_trade_no = params.delete(:combine_out_trade_no) 113 | 114 | url = "/v3/combine-transactions/out-trade-no/#{combine_out_trade_no}" 115 | 116 | method = 'GET' 117 | 118 | make_request( 119 | method: method, 120 | path: url, 121 | extra_headers: { 122 | 'Content-Type' => 'application/x-www-form-urlencoded' 123 | } 124 | ) 125 | end 126 | 127 | CLOSE_COMBINE_ORDER_FIELDS = %i[combine_out_trade_no sub_orders].freeze # :nodoc: 128 | # 129 | # 关闭合单 130 | # 131 | # TODO: 与电商平台相同,稍后重构 132 | # 133 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter7_3_11.shtml 134 | # 135 | # ``` ruby 136 | # WechatPay::Direct.close_combine_order(combine_out_trade_no: 'C202104302474') 137 | # ``` 138 | def self.close_combine_order(params) 139 | combine_out_trade_no = params.delete(:combine_out_trade_no) 140 | 141 | url = "/v3/combine-transactions/out-trade-no/#{combine_out_trade_no}/close" 142 | 143 | payload = { 144 | combine_appid: WechatPay.app_id 145 | }.merge(params) 146 | 147 | payload_json = payload.to_json 148 | 149 | method = 'POST' 150 | 151 | make_request( 152 | method: method, 153 | for_sign: payload_json, 154 | payload: payload_json, 155 | path: url 156 | ) 157 | end 158 | 159 | QUERY_ORDER_FIELDS = %i[out_trade_no transaction_id].freeze # :nodoc: 160 | # 161 | # 直连订单查询 162 | # 163 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_2.shtml 164 | # 165 | # Example: 166 | # 167 | # ``` ruby 168 | # WechatPay::Direct.query_order(transaction_id: '4323400972202104305133344444') # by transaction_id 169 | # WechatPay::Direct.query_order(out_trade_no: 'N202104302474') # by out_trade_no 170 | # ``` 171 | # 172 | def self.query_order(params) 173 | if params[:transaction_id] 174 | params.delete(:out_trade_no) 175 | transaction_id = params.delete(:transaction_id) 176 | path = "/v3/pay/transactions/id/#{transaction_id}" 177 | else 178 | params.delete(:transaction_id) 179 | out_trade_no = params.delete(:out_trade_no) 180 | path = "/v3/pay/transactions/out-trade-no/#{out_trade_no}" 181 | end 182 | 183 | params = params.merge({ 184 | mchid: WechatPay.mch_id 185 | }) 186 | 187 | method = 'GET' 188 | query = build_query(params) 189 | url = "#{path}?#{query}" 190 | 191 | make_request( 192 | method: method, 193 | path: url, 194 | extra_headers: { 195 | 'Content-Type' => 'application/x-www-form-urlencoded' 196 | } 197 | ) 198 | end 199 | 200 | CLOSE_ORDER_FIELDS = %i[out_trade_no].freeze # :nodoc: 201 | # 202 | # 关闭订单 203 | # 204 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_3.shtml 205 | # 206 | # Example: 207 | # 208 | # ``` ruby 209 | # WechatPay::Direct.close_order(out_trade_no: 'N3344445') 210 | # ``` 211 | # 212 | def self.close_order(params) 213 | out_trade_no = params.delete(:out_trade_no) 214 | url = "/v3/pay/transactions/out-trade-no/#{out_trade_no}/close" 215 | params = params.merge({ 216 | mchid: WechatPay.mch_id 217 | }) 218 | 219 | method = 'POST' 220 | 221 | make_request( 222 | method: method, 223 | path: url, 224 | for_sign: params.to_json, 225 | payload: params.to_json 226 | ) 227 | end 228 | 229 | INVOKE_REFUND_FIELDS = %i[transaction_id out_trade_no out_refund_no amount].freeze # :nodoc: 230 | # 231 | # 退款申请 232 | # 233 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_9.shtml 234 | # 235 | # Example: 236 | # 237 | # ``` ruby 238 | # WechatPay::Direct.invoke_refund(transaction_id: '4323400972202104305131070170', total: 1, refund: 1, out_refund_no: 'R10000') 239 | # WechatPay::Direct.invoke_refund(out_trade_no: 'N2021', total: 1, refund: 1, out_refund_no: 'R10000').body 240 | # ``` 241 | def self.invoke_refund(params) 242 | url = '/v3/refund/domestic/refunds' 243 | method = 'POST' 244 | amount = { 245 | refund: params.delete(:refund), 246 | total: params.delete(:total), 247 | currency: 'CNY' 248 | } 249 | 250 | params = params.merge({ 251 | amount: amount 252 | }) 253 | 254 | make_request( 255 | path: url, 256 | method: method, 257 | for_sign: params.to_json, 258 | payload: params.to_json 259 | ) 260 | end 261 | 262 | QUERY_REFUND_FIELDS = %i[sub_mchid refund_id out_refund_no].freeze # :nodoc: 263 | # 264 | # 直连退款查询 265 | # 266 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_10.shtml 267 | # 268 | # Example: 269 | # 270 | # ``` ruby 271 | # WechatPay::Direct.query_refund(out_refund_no: 'R10000') 272 | # ``` 273 | # 274 | def self.query_refund(params) 275 | out_refund_no = params.delete(:out_refund_no) 276 | url = "/v3/refund/domestic/refunds/#{out_refund_no}" 277 | 278 | method = 'GET' 279 | 280 | make_request( 281 | method: method, 282 | path: url, 283 | extra_headers: { 284 | 'Content-Type' => 'application/x-www-form-urlencoded' 285 | } 286 | ) 287 | end 288 | 289 | TRADEBILL_FIELDS = [:bill_date].freeze # :nodoc: 290 | # 291 | # 直连申请交易账单 292 | # 293 | # Todo: 跟商户平台接口相同 294 | # 295 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_6.shtml 296 | # 297 | # Example: 298 | # 299 | # ``` ruby 300 | # WechatPay::direct.tradebill(bill_date: '2021-04-30') 301 | # ``` 302 | def self.tradebill(params) 303 | path = '/v3/bill/tradebill' 304 | method = 'GET' 305 | 306 | query = build_query(params) 307 | url = "#{path}?#{query}" 308 | 309 | make_request( 310 | path: url, 311 | method: method, 312 | extra_headers: { 313 | 'Content-Type' => 'application/x-www-form-urlencoded' 314 | } 315 | ) 316 | end 317 | 318 | FUNDFLOWBILL_FIELDS = [:bill_date].freeze # :nodoc: 319 | # 320 | # 申请资金账单 321 | # 322 | # Todo: 跟商户平台接口相同 323 | # 324 | # Document: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_7.shtml 325 | # 326 | # Example: 327 | # 328 | # ``` ruby 329 | # WechatPay::Direct.fundflowbill(bill_date: '2021-04-30') 330 | # ``` 331 | # 332 | def self.fundflowbill(params) 333 | path = '/v3/bill/fundflowbill' 334 | method = 'GET' 335 | 336 | query = build_query(params) 337 | url = "#{path}?#{query}" 338 | 339 | make_request( 340 | path: url, 341 | method: method, 342 | extra_headers: { 343 | 'Content-Type' => 'application/x-www-form-urlencoded' 344 | } 345 | ) 346 | end 347 | 348 | class << self 349 | private 350 | 351 | def direct_transactions_method_by_suffix(suffix, params) 352 | url = "/v3/pay/transactions/#{suffix}" 353 | method = 'POST' 354 | 355 | params = { 356 | mchid: WechatPay.mch_id, 357 | appid: WechatPay.app_id 358 | }.merge(params) 359 | 360 | payload_json = params.to_json 361 | 362 | make_request( 363 | method: method, 364 | path: url, 365 | for_sign: payload_json, 366 | payload: payload_json 367 | ) 368 | end 369 | end 370 | end 371 | end 372 | --------------------------------------------------------------------------------